JAL-2835 refactored to extract specific consequence term per transcript and allele
[jalview.git] / src / jalview / io / vcf / VCFLoader.java
1 package jalview.io.vcf;
2
3 import jalview.analysis.AlignmentUtils;
4 import jalview.analysis.Dna;
5 import jalview.api.AlignViewControllerGuiI;
6 import jalview.bin.Cache;
7 import jalview.datamodel.AlignmentI;
8 import jalview.datamodel.DBRefEntry;
9 import jalview.datamodel.GeneLociI;
10 import jalview.datamodel.Mapping;
11 import jalview.datamodel.SequenceFeature;
12 import jalview.datamodel.SequenceI;
13 import jalview.datamodel.features.FeatureAttributeType;
14 import jalview.datamodel.features.FeatureSource;
15 import jalview.datamodel.features.FeatureSources;
16 import jalview.ext.ensembl.EnsemblMap;
17 import jalview.ext.htsjdk.VCFReader;
18 import jalview.io.gff.Gff3Helper;
19 import jalview.io.gff.SequenceOntologyI;
20 import jalview.util.MapList;
21 import jalview.util.MappingUtils;
22 import jalview.util.MessageManager;
23
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.regex.Pattern;
31 import java.util.regex.PatternSyntaxException;
32
33 import htsjdk.samtools.SAMException;
34 import htsjdk.samtools.SAMSequenceDictionary;
35 import htsjdk.samtools.SAMSequenceRecord;
36 import htsjdk.samtools.util.CloseableIterator;
37 import htsjdk.variant.variantcontext.Allele;
38 import htsjdk.variant.variantcontext.VariantContext;
39 import htsjdk.variant.vcf.VCFHeader;
40 import htsjdk.variant.vcf.VCFHeaderLine;
41 import htsjdk.variant.vcf.VCFHeaderLineCount;
42 import htsjdk.variant.vcf.VCFHeaderLineType;
43 import htsjdk.variant.vcf.VCFInfoHeaderLine;
44
45 /**
46  * A class to read VCF data (using the htsjdk) and add variants as sequence
47  * features on dna and any related protein product sequences
48  * 
49  * @author gmcarstairs
50  */
51 public class VCFLoader
52 {
53   /**
54    * A class to model the mapping from sequence to VCF coordinates. Cases include
55    * <ul>
56    * <li>a direct 1:1 mapping where the sequence is one of the VCF contigs</li>
57    * <li>a mapping of sequence to chromosomal coordinates, where sequence and VCF
58    * use the same reference assembly</li>
59    * <li>a modified mapping of sequence to chromosomal coordinates, where sequence
60    * and VCF use different reference assembles</li>
61    * </ul>
62    */
63   class VCFMap
64   {
65     final String chromosome;
66
67     final MapList map;
68
69     VCFMap(String chr, MapList m)
70     {
71       chromosome = chr;
72       map = m;
73     }
74
75     @Override
76     public String toString()
77     {
78       return chromosome + ":" + map.toString();
79     }
80   }
81
82   /*
83    * Lookup keys, and default values, for Preference entries that describe
84    * patterns for VCF and VEP fields to capture 
85    */
86   private static final String VEP_FIELDS_PREF = "VEP_FIELDS";
87
88   private static final String VCF_FIELDS_PREF = "VCF_FIELDS";
89
90   private static final String DEFAULT_VCF_FIELDS = ".*";
91
92   private static final String DEFAULT_VEP_FIELDS = ".*";// "Allele,Consequence,IMPACT,SWISSPROT,SIFT,PolyPhen,CLIN_SIG";
93
94   /*
95    * keys to fields of VEP CSQ consequence data
96    * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html
97    */
98   private static final String CSQ_CONSEQUENCE_KEY = "Consequence";
99   private static final String CSQ_ALLELE_KEY = "Allele";
100   private static final String CSQ_ALLELE_NUM_KEY = "ALLELE_NUM"; // 0 (ref), 1...
101   private static final String CSQ_FEATURE_KEY = "Feature"; // Ensembl stable id
102
103   /*
104    * default VCF INFO key for VEP consequence data
105    * NB this can be overridden running VEP with --vcf_info_field
106    * - we don't handle this case (require identifier to be CSQ)
107    */
108   private static final String CSQ_FIELD = "CSQ";
109
110   /*
111    * separator for fields in consequence data is '|'
112    */
113   private static final String PIPE_REGEX = "\\|";
114
115   /*
116    * key for Allele Frequency output by VEP
117    * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
118    */
119   private static final String ALLELE_FREQUENCY_KEY = "AF";
120
121   /*
122    * delimiter that separates multiple consequence data blocks
123    */
124   private static final String COMMA = ",";
125
126   /*
127    * the feature group assigned to a VCF variant in Jalview
128    */
129   private static final String FEATURE_GROUP_VCF = "VCF";
130
131   /*
132    * internal delimiter used to build keys for assemblyMappings
133    * 
134    */
135   private static final String EXCL = "!";
136
137   /*
138    * the alignment we are associating VCF data with
139    */
140   private AlignmentI al;
141
142   /*
143    * mappings between VCF and sequence reference assembly regions, as 
144    * key = "species!chromosome!fromAssembly!toAssembly
145    * value = Map{fromRange, toRange}
146    */
147   private Map<String, Map<int[], int[]>> assemblyMappings;
148
149   /*
150    * holds details of the VCF header lines (metadata)
151    */
152   private VCFHeader header;
153
154   /*
155    * a Dictionary of contigs (if present) referenced in the VCF file
156    */
157   private SAMSequenceDictionary dictionary;
158
159   /*
160    * the position (0...) of field in each block of
161    * CSQ (consequence) data (if declared in the VCF INFO header for CSQ)
162    * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
163    */
164   private int csqConsequenceFieldIndex = -1;
165   private int csqAlleleFieldIndex = -1;
166   private int csqAlleleNumberFieldIndex = -1;
167   private int csqFeatureFieldIndex = -1;
168
169   // todo the same fields for SnpEff ANN data if wanted
170   // see http://snpeff.sourceforge.net/SnpEff_manual.html#input
171
172   /*
173    * a unique identifier under which to save metadata about feature
174    * attributes (selected INFO field data)
175    */
176   private String sourceId;
177
178   /*
179    * The INFO IDs of data that is both present in the VCF file, and
180    * also matched by any filters for data of interest
181    */
182   List<String> vcfFieldsOfInterest;
183
184   /*
185    * The field offsets and identifiers for VEP (CSQ) data that is both present
186    * in the VCF file, and also matched by any filters for data of interest
187    * for example 0 -> Allele, 1 -> Consequence, ..., 36 -> SIFT, ...
188    */
189   Map<Integer, String> vepFieldsOfInterest;
190
191   /**
192    * Constructor given an alignment context
193    * 
194    * @param alignment
195    */
196   public VCFLoader(AlignmentI alignment)
197   {
198     al = alignment;
199
200     // map of species!chromosome!fromAssembly!toAssembly to {fromRange, toRange}
201     assemblyMappings = new HashMap<>();
202   }
203
204   /**
205    * Starts a new thread to query and load VCF variant data on to the alignment
206    * <p>
207    * This method is not thread safe - concurrent threads should use separate
208    * instances of this class.
209    * 
210    * @param filePath
211    * @param gui
212    */
213   public void loadVCF(final String filePath,
214           final AlignViewControllerGuiI gui)
215   {
216     if (gui != null)
217     {
218       gui.setStatus(MessageManager.getString("label.searching_vcf"));
219     }
220
221     new Thread()
222     {
223
224       @Override
225       public void run()
226       {
227         VCFLoader.this.doLoad(filePath, gui);
228       }
229
230     }.start();
231   }
232
233   /**
234    * Loads VCF on to an alignment - provided it can be related to one or more
235    * sequence's chromosomal coordinates
236    * 
237    * @param filePath
238    * @param gui
239    *          optional callback handler for messages
240    */
241   protected void doLoad(String filePath, AlignViewControllerGuiI gui)
242   {
243     VCFReader reader = null;
244     try
245     {
246       // long start = System.currentTimeMillis();
247       reader = new VCFReader(filePath);
248
249       header = reader.getFileHeader();
250
251       try
252       {
253         dictionary = header.getSequenceDictionary();
254       } catch (SAMException e)
255       {
256         // ignore - thrown if any contig line lacks length info
257       }
258
259       sourceId = filePath;
260
261       saveMetadata(sourceId);
262
263       /*
264        * get offset of CSQ ALLELE_NUM and Feature if declared
265        */
266       parseCsqHeader();
267
268       VCFHeaderLine ref = header
269               .getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
270       String vcfAssembly = ref.getValue();
271
272       int varCount = 0;
273       int seqCount = 0;
274
275       /*
276        * query for VCF overlapping each sequence in turn
277        */
278       for (SequenceI seq : al.getSequences())
279       {
280         int added = loadSequenceVCF(seq, reader, vcfAssembly);
281         if (added > 0)
282         {
283           seqCount++;
284           varCount += added;
285           transferAddedFeatures(seq);
286         }
287       }
288       if (gui != null)
289       {
290         // long elapsed = System.currentTimeMillis() - start;
291         String msg = MessageManager.formatMessage("label.added_vcf",
292                 varCount, seqCount);
293         gui.setStatus(msg);
294         if (gui.getFeatureSettingsUI() != null)
295         {
296           gui.getFeatureSettingsUI().discoverAllFeatureData();
297         }
298       }
299     } catch (Throwable e)
300     {
301       System.err.println("Error processing VCF: " + e.getMessage());
302       e.printStackTrace();
303       if (gui != null)
304       {
305         gui.setStatus("Error occurred - see console for details");
306       }
307     } finally
308     {
309       if (reader != null)
310       {
311         try
312         {
313           reader.close();
314         } catch (IOException e)
315         {
316           // ignore
317         }
318       }
319       header = null;
320       dictionary = null;
321     }
322   }
323
324   /**
325    * Reads metadata (such as INFO field descriptions and datatypes) and saves
326    * them for future reference
327    * 
328    * @param theSourceId
329    */
330   void saveMetadata(String theSourceId)
331   {
332     List<Pattern> vcfFieldPatterns = getFieldMatchers(VCF_FIELDS_PREF,
333             DEFAULT_VCF_FIELDS);
334     vcfFieldsOfInterest = new ArrayList<>();
335
336     FeatureSource metadata = new FeatureSource(theSourceId);
337
338     for (VCFInfoHeaderLine info : header.getInfoHeaderLines())
339     {
340       String attributeId = info.getID();
341       String desc = info.getDescription();
342       VCFHeaderLineType type = info.getType();
343       FeatureAttributeType attType = null;
344       switch (type)
345       {
346       case Character:
347         attType = FeatureAttributeType.Character;
348         break;
349       case Flag:
350         attType = FeatureAttributeType.Flag;
351         break;
352       case Float:
353         attType = FeatureAttributeType.Float;
354         break;
355       case Integer:
356         attType = FeatureAttributeType.Integer;
357         break;
358       case String:
359         attType = FeatureAttributeType.String;
360         break;
361       }
362       metadata.setAttributeName(attributeId, desc);
363       metadata.setAttributeType(attributeId, attType);
364
365       if (isFieldWanted(attributeId, vcfFieldPatterns))
366       {
367         vcfFieldsOfInterest.add(attributeId);
368       }
369     }
370
371     FeatureSources.getInstance().addSource(theSourceId, metadata);
372   }
373
374   /**
375    * Answers true if the field id is matched by any of the filter patterns, else
376    * false. Matching is against regular expression patterns, and is not
377    * case-sensitive.
378    * 
379    * @param id
380    * @param filters
381    * @return
382    */
383   private boolean isFieldWanted(String id, List<Pattern> filters)
384   {
385     for (Pattern p : filters)
386     {
387       if (p.matcher(id.toUpperCase()).matches())
388       {
389         return true;
390       }
391     }
392     return false;
393   }
394
395   /**
396    * Records 'wanted' fields defined in the CSQ INFO header (if there is one).
397    * Also records the position of selected fields (Allele, ALLELE_NUM, Feature)
398    * required for processing.
399    * <p>
400    * CSQ fields are declared in the CSQ INFO Description e.g.
401    * <p>
402    * Description="Consequence ...from ... VEP. Format: Allele|Consequence|...
403    */
404   protected void parseCsqHeader()
405   {
406     List<Pattern> vepFieldFilters = getFieldMatchers(VEP_FIELDS_PREF,
407             DEFAULT_VEP_FIELDS);
408     vepFieldsOfInterest = new HashMap<>();
409
410     VCFInfoHeaderLine csqInfo = header.getInfoHeaderLine(CSQ_FIELD);
411     if (csqInfo == null)
412     {
413       return;
414     }
415
416     /*
417      * parse out the pipe-separated list of CSQ fields; we assume here that
418      * these form the last part of the description, and contain no spaces
419      */
420     String desc = csqInfo.getDescription();
421     int spacePos = desc.lastIndexOf(" ");
422     desc = desc.substring(spacePos + 1);
423
424     if (desc != null)
425     {
426       String[] format = desc.split(PIPE_REGEX);
427       int index = 0;
428       for (String field : format)
429       {
430         if (CSQ_CONSEQUENCE_KEY.equals(field))
431         {
432           csqConsequenceFieldIndex = index;
433         }
434         if (CSQ_ALLELE_NUM_KEY.equals(field))
435         {
436           csqAlleleNumberFieldIndex = index;
437         }
438         if (CSQ_ALLELE_KEY.equals(field))
439         {
440           csqAlleleFieldIndex = index;
441         }
442         if (CSQ_FEATURE_KEY.equals(field))
443         {
444           csqFeatureFieldIndex = index;
445         }
446
447         if (isFieldWanted(field, vepFieldFilters))
448         {
449           vepFieldsOfInterest.put(index, field);
450         }
451
452         index++;
453       }
454     }
455   }
456
457   /**
458    * Reads the Preference value for the given key, with default specified if no
459    * preference set. The value is interpreted as a comma-separated list of
460    * regular expressions, and converted into a list of compiled patterns ready
461    * for matching. Patterns are forced to upper-case for non-case-sensitive
462    * matching.
463    * <p>
464    * This supports user-defined filters for fields of interest to capture while
465    * processing data. For example, VCF_FIELDS = AF,AC* would mean that VCF INFO
466    * fields with an ID of AF, or starting with AC, would be matched.
467    * 
468    * @param key
469    * @param def
470    * @return
471    */
472   private List<Pattern> getFieldMatchers(String key, String def)
473   {
474     String pref = Cache.getDefault(key, def);
475     List<Pattern> patterns = new ArrayList<>();
476     String[] tokens = pref.split(",");
477     for (String token : tokens)
478     {
479       try
480       {
481       patterns.add(Pattern.compile(token.toUpperCase()));
482       } catch (PatternSyntaxException e)
483       {
484         System.err.println("Invalid pattern ignored: " + token);
485       }
486     }
487     return patterns;
488   }
489
490   /**
491    * Transfers VCF features to sequences to which this sequence has a mapping.
492    * If the mapping is 3:1, computes peptide variants from nucleotide variants.
493    * 
494    * @param seq
495    */
496   protected void transferAddedFeatures(SequenceI seq)
497   {
498     DBRefEntry[] dbrefs = seq.getDBRefs();
499     if (dbrefs == null)
500     {
501       return;
502     }
503     for (DBRefEntry dbref : dbrefs)
504     {
505       Mapping mapping = dbref.getMap();
506       if (mapping == null || mapping.getTo() == null)
507       {
508         continue;
509       }
510
511       SequenceI mapTo = mapping.getTo();
512       MapList map = mapping.getMap();
513       if (map.getFromRatio() == 3)
514       {
515         /*
516          * dna-to-peptide product mapping
517          */
518         AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
519       }
520       else
521       {
522         /*
523          * nucleotide-to-nucleotide mapping e.g. transcript to CDS
524          */
525         List<SequenceFeature> features = seq.getFeatures()
526                 .getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT);
527         for (SequenceFeature sf : features)
528         {
529           if (FEATURE_GROUP_VCF.equals(sf.getFeatureGroup()))
530           {
531             transferFeature(sf, mapTo, map);
532           }
533         }
534       }
535     }
536   }
537
538   /**
539    * Tries to add overlapping variants read from a VCF file to the given
540    * sequence, and returns the number of variant features added. Note that this
541    * requires the sequence to hold information as to its species, chromosomal
542    * positions and reference assembly, in order to be able to map the VCF
543    * variants to the sequence (or not)
544    * 
545    * @param seq
546    * @param reader
547    * @param vcfAssembly
548    * @return
549    */
550   protected int loadSequenceVCF(SequenceI seq, VCFReader reader,
551           String vcfAssembly)
552   {
553     VCFMap vcfMap = getVcfMap(seq, vcfAssembly);
554     if (vcfMap == null)
555     {
556       return 0;
557     }
558
559     return addVcfVariants(seq, reader, vcfMap, vcfAssembly);
560   }
561
562   /**
563    * Answers a map from sequence coordinates to VCF chromosome ranges
564    * 
565    * @param seq
566    * @param vcfAssembly
567    * @return
568    */
569   private VCFMap getVcfMap(SequenceI seq, String vcfAssembly)
570   {
571     /*
572      * simplest case: sequence has id and length matching a VCF contig
573      */
574     VCFMap vcfMap = null;
575     if (dictionary != null)
576     {
577       vcfMap = getContigMap(seq);
578     }
579     if (vcfMap != null)
580     {
581       return vcfMap;
582     }
583
584     /*
585      * otherwise, map to VCF from chromosomal coordinates 
586      * of the sequence (if known)
587      */
588     GeneLociI seqCoords = seq.getGeneLoci();
589     if (seqCoords == null)
590     {
591       Cache.log.warn(String.format(
592               "Can't query VCF for %s as chromosome coordinates not known",
593               seq.getName()));
594       return null;
595     }
596
597     String species = seqCoords.getSpeciesId();
598     String chromosome = seqCoords.getChromosomeId();
599     String seqRef = seqCoords.getAssemblyId();
600     MapList map = seqCoords.getMap();
601
602     if (!vcfSpeciesMatchesSequence(vcfAssembly, species))
603     {
604       return null;
605     }
606
607     if (vcfAssemblyMatchesSequence(vcfAssembly, seqRef))
608     {
609       return new VCFMap(chromosome, map);
610     }
611
612     if (!"GRCh38".equalsIgnoreCase(seqRef) // Ensembl
613             || !vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
614     {
615       return null;
616     }
617
618     /*
619      * map chromosomal coordinates from sequence to VCF if the VCF
620      * data has a different reference assembly to the sequence
621      */
622     // TODO generalise for cases other than GRCh38 -> GRCh37 !
623     // - or get the user to choose in a dialog
624
625     List<int[]> toVcfRanges = new ArrayList<>();
626     List<int[]> fromSequenceRanges = new ArrayList<>();
627     String toRef = "GRCh37";
628
629     for (int[] range : map.getToRanges())
630     {
631       int[] fromRange = map.locateInFrom(range[0], range[1]);
632       if (fromRange == null)
633       {
634         // corrupted map?!?
635         continue;
636       }
637
638       int[] newRange = mapReferenceRange(range, chromosome, "human", seqRef,
639               toRef);
640       if (newRange == null)
641       {
642         Cache.log.error(
643                 String.format("Failed to map %s:%s:%s:%d:%d to %s", species,
644                         chromosome, seqRef, range[0], range[1], toRef));
645         continue;
646       }
647       else
648       {
649         toVcfRanges.add(newRange);
650         fromSequenceRanges.add(fromRange);
651       }
652     }
653
654     return new VCFMap(chromosome,
655             new MapList(fromSequenceRanges, toVcfRanges, 1, 1));
656   }
657
658   /**
659    * If the sequence id matches a contig declared in the VCF file, and the
660    * sequence length matches the contig length, then returns a 1:1 map of the
661    * sequence to the contig, else returns null
662    * 
663    * @param seq
664    * @return
665    */
666   private VCFMap getContigMap(SequenceI seq)
667   {
668     String id = seq.getName();
669     SAMSequenceRecord contig = dictionary.getSequence(id);
670     if (contig != null)
671     {
672       int len = seq.getLength();
673       if (len == contig.getSequenceLength())
674       {
675         MapList map = new MapList(new int[] { 1, len },
676                 new int[]
677                 { 1, len }, 1, 1);
678         return new VCFMap(id, map);
679       }
680     }
681     return null;
682   }
683
684   /**
685    * Answers true if we determine that the VCF data uses the same reference
686    * assembly as the sequence, else false
687    * 
688    * @param vcfAssembly
689    * @param seqRef
690    * @return
691    */
692   private boolean vcfAssemblyMatchesSequence(String vcfAssembly,
693           String seqRef)
694   {
695     // TODO improve on this stub, which handles gnomAD and
696     // hopes for the best for other cases
697
698     if ("GRCh38".equalsIgnoreCase(seqRef) // Ensembl
699             && vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
700     {
701       return false;
702     }
703     return true;
704   }
705
706   /**
707    * Answers true if the species inferred from the VCF reference identifier
708    * matches that for the sequence
709    * 
710    * @param vcfAssembly
711    * @param speciesId
712    * @return
713    */
714   boolean vcfSpeciesMatchesSequence(String vcfAssembly, String speciesId)
715   {
716     // PROBLEM 1
717     // there are many aliases for species - how to equate one with another?
718     // PROBLEM 2
719     // VCF ##reference header is an unstructured URI - how to extract species?
720     // perhaps check if ref includes any (Ensembl) alias of speciesId??
721     // TODO ask the user to confirm this??
722
723     if (vcfAssembly.contains("Homo_sapiens") // gnomAD exome data example
724             && "HOMO_SAPIENS".equals(speciesId)) // Ensembl species id
725     {
726       return true;
727     }
728
729     if (vcfAssembly.contains("c_elegans") // VEP VCF response example
730             && "CAENORHABDITIS_ELEGANS".equals(speciesId)) // Ensembl
731     {
732       return true;
733     }
734
735     // this is not a sustainable solution...
736
737     return false;
738   }
739
740   /**
741    * Queries the VCF reader for any variants that overlap the mapped chromosome
742    * ranges of the sequence, and adds as variant features. Returns the number of
743    * overlapping variants found.
744    * 
745    * @param seq
746    * @param reader
747    * @param map
748    *          mapping from sequence to VCF coordinates
749    * @param vcfAssembly
750    *          the '##reference' identifier for the VCF reference assembly
751    * @return
752    */
753   protected int addVcfVariants(SequenceI seq, VCFReader reader,
754           VCFMap map, String vcfAssembly)
755   {
756     boolean forwardStrand = map.map.isToForwardStrand();
757
758     /*
759      * query the VCF for overlaps of each contiguous chromosomal region
760      */
761     int count = 0;
762
763     for (int[] range : map.map.getToRanges())
764     {
765       int vcfStart = Math.min(range[0], range[1]);
766       int vcfEnd = Math.max(range[0], range[1]);
767       CloseableIterator<VariantContext> variants = reader
768               .query(map.chromosome, vcfStart, vcfEnd);
769       while (variants.hasNext())
770       {
771         VariantContext variant = variants.next();
772
773         int[] featureRange = map.map.locateInFrom(variant.getStart(),
774                 variant.getEnd());
775
776         if (featureRange != null)
777         {
778           int featureStart = Math.min(featureRange[0], featureRange[1]);
779           int featureEnd = Math.max(featureRange[0], featureRange[1]);
780           count += addAlleleFeatures(seq, variant, featureStart, featureEnd,
781                   forwardStrand);
782         }
783       }
784       variants.close();
785     }
786
787     return count;
788   }
789
790   /**
791    * A convenience method to get the AF value for the given alternate allele
792    * index
793    * 
794    * @param variant
795    * @param alleleIndex
796    * @return
797    */
798   protected float getAlleleFrequency(VariantContext variant, int alleleIndex)
799   {
800     float score = 0f;
801     String attributeValue = getAttributeValue(variant,
802             ALLELE_FREQUENCY_KEY, alleleIndex);
803     if (attributeValue != null)
804     {
805       try
806       {
807         score = Float.parseFloat(attributeValue);
808       } catch (NumberFormatException e)
809       {
810         // leave as 0
811       }
812     }
813
814     return score;
815   }
816
817   /**
818    * A convenience method to get an attribute value for an alternate allele
819    * 
820    * @param variant
821    * @param attributeName
822    * @param alleleIndex
823    * @return
824    */
825   protected String getAttributeValue(VariantContext variant,
826           String attributeName, int alleleIndex)
827   {
828     Object att = variant.getAttribute(attributeName);
829
830     if (att instanceof String)
831     {
832       return (String) att;
833     }
834     else if (att instanceof ArrayList)
835     {
836       return ((List<String>) att).get(alleleIndex);
837     }
838
839     return null;
840   }
841
842   /**
843    * Adds one variant feature for each allele in the VCF variant record, and
844    * returns the number of features added.
845    * 
846    * @param seq
847    * @param variant
848    * @param featureStart
849    * @param featureEnd
850    * @param forwardStrand
851    * @return
852    */
853   protected int addAlleleFeatures(SequenceI seq, VariantContext variant,
854           int featureStart, int featureEnd, boolean forwardStrand)
855   {
856     int added = 0;
857
858     /*
859      * Javadoc says getAlternateAlleles() imposes no order on the list returned
860      * so we proceed defensively to get them in strict order
861      */
862     int altAlleleCount = variant.getAlternateAlleles().size();
863     for (int i = 0; i < altAlleleCount; i++)
864     {
865       added += addAlleleFeature(seq, variant, i, featureStart, featureEnd,
866               forwardStrand);
867     }
868     return added;
869   }
870
871   /**
872    * Inspects one allele and attempts to add a variant feature for it to the
873    * sequence. The additional data associated with this allele is extracted to
874    * store in the feature's key-value map. Answers the number of features added (0
875    * or 1).
876    * 
877    * @param seq
878    * @param variant
879    * @param altAlleleIndex
880    *          (0, 1..)
881    * @param featureStart
882    * @param featureEnd
883    * @param forwardStrand
884    * @return
885    */
886   protected int addAlleleFeature(SequenceI seq, VariantContext variant,
887           int altAlleleIndex, int featureStart, int featureEnd,
888           boolean forwardStrand)
889   {
890     String reference = variant.getReference().getBaseString();
891     Allele alt = variant.getAlternateAllele(altAlleleIndex);
892     String allele = alt.getBaseString();
893
894     /*
895      * build the ref,alt allele description e.g. "G,A", using the base
896      * complement if the sequence is on the reverse strand
897      */
898     // FIXME correctly handle insertions on reverse strand JAL-2845
899     StringBuilder sb = new StringBuilder();
900     sb.append(forwardStrand ? reference : Dna.reverseComplement(reference));
901     sb.append(COMMA);
902     sb.append(forwardStrand ? allele : Dna.reverseComplement(allele));
903     String alleles = sb.toString(); // e.g. G,A
904
905     String type = getOntologyTerm(seq, variant, altAlleleIndex);
906
907     float score = getAlleleFrequency(variant, altAlleleIndex);
908
909     SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
910             featureEnd, score, FEATURE_GROUP_VCF);
911     sf.setSource(sourceId);
912
913     sf.setValue(Gff3Helper.ALLELES, alleles);
914
915     addAlleleProperties(variant, seq, sf, altAlleleIndex);
916
917     seq.addSequenceFeature(sf);
918
919     return 1;
920   }
921
922   /**
923    * Determines the Sequence Ontology term to use for the variant feature type in
924    * Jalview. The default is 'sequence_variant', but a more specific term is used
925    * if:
926    * <ul>
927    * <li>VEP (or SnpEff) Consequence annotation is included in the VCF</li>
928    * <li>sequence id can be matched to VEP Feature (or SnpEff Feature_ID)</li>
929    * </ul>
930    * 
931    * @param seq
932    * @param variant
933    * @param altAlleleIndex
934    * @return
935    * @see http://www.sequenceontology.org/browser/current_svn/term/SO:0001060
936    */
937   String getOntologyTerm(SequenceI seq, VariantContext variant,
938           int altAlleleIndex)
939   {
940     String type = SequenceOntologyI.SEQUENCE_VARIANT;
941
942     if (csqAlleleFieldIndex == -1) // && snpEffAlleleFieldIndex == -1
943     {
944       /*
945        * no Consequence data so we can't refine the ontology term
946        */
947       return type;
948     }
949
950     /*
951      * can we associate Consequence data with this allele and feature (transcript)?
952      * if so, prefer the consequence term from that data
953      */
954     String consequence = getConsequenceForAlleleAndFeature(variant,
955             CSQ_FIELD,
956             altAlleleIndex, csqAlleleFieldIndex, csqAlleleNumberFieldIndex,
957             seq.getName().toLowerCase(), csqFeatureFieldIndex);
958     if (consequence != null)
959     {
960       String[] csqFields = consequence.split(PIPE_REGEX);
961       if (csqFields.length > csqConsequenceFieldIndex)
962       {
963         type = csqFields[csqConsequenceFieldIndex];
964       }
965     }
966     else
967     {
968       // todo the same for SnpEff consequence data matching if wanted
969     }
970
971     /*
972      * if of the form (e.g.) missense_variant&splice_region_variant,
973      * just take the first ('most severe') consequence
974      */
975     if (type != null)
976     {
977       int pos = type.indexOf('&');
978       if (pos > 0)
979       {
980         type = type.substring(0, pos);
981       }
982     }
983     return type;
984   }
985
986   /**
987    * Returns matched consequence data if it can be found, else null.
988    * <ul>
989    * <li>inspects the VCF data for key 'vcfInfoId'</li>
990    * <li>splits this on comma (to distinct consequences)</li>
991    * <li>returns the first consequence (if any) where</li>
992    * <ul>
993    * <li>the allele matches the altAlleleIndex'th allele of variant</li>
994    * <li>the feature matches the sequence name (e.g. transcript id)</li>
995    * </ul>
996    * </ul>
997    * If matched, the consequence is returned (as pipe-delimited fields).
998    * 
999    * @param variant
1000    * @param vcfInfoId
1001    * @param altAlleleIndex
1002    * @param alleleFieldIndex
1003    * @param alleleNumberFieldIndex
1004    * @param seqName
1005    * @param featureFieldIndex
1006    * @return
1007    */
1008   private String getConsequenceForAlleleAndFeature(VariantContext variant,
1009           String vcfInfoId, int altAlleleIndex, int alleleFieldIndex,
1010           int alleleNumberFieldIndex,
1011           String seqName, int featureFieldIndex)
1012   {
1013     if (alleleFieldIndex == -1 || featureFieldIndex == -1)
1014     {
1015       return null;
1016     }
1017     Object value = variant.getAttribute(vcfInfoId);
1018
1019     if (value == null || !(value instanceof List<?>))
1020     {
1021       return null;
1022     }
1023
1024     /*
1025      * inspect each consequence in turn (comma-separated blocks
1026      * extracted by htsjdk)
1027      */
1028     List<String> consequences = (List<String>) value;
1029
1030     for (String consequence : consequences)
1031     {
1032       String[] csqFields = consequence.split(PIPE_REGEX);
1033       if (csqFields.length > featureFieldIndex)
1034       {
1035         String featureIdentifier = csqFields[featureFieldIndex];
1036         if (featureIdentifier.length() > 4
1037                 && seqName.indexOf(featureIdentifier.toLowerCase()) > -1)
1038         {
1039           /*
1040            * feature (transcript) matched - now check for allele match
1041            */
1042           if (matchAllele(variant, altAlleleIndex, csqFields,
1043                   alleleFieldIndex, alleleNumberFieldIndex))
1044           {
1045             return consequence;
1046           }
1047         }
1048       }
1049     }
1050     return null;
1051   }
1052
1053   private boolean matchAllele(VariantContext variant, int altAlleleIndex,
1054           String[] csqFields, int alleleFieldIndex,
1055           int alleleNumberFieldIndex)
1056   {
1057     /*
1058      * if ALLELE_NUM is present, it must match altAlleleIndex
1059      * NB first alternate allele is 1 for ALLELE_NUM, 0 for altAlleleIndex
1060      */
1061     if (alleleNumberFieldIndex > -1)
1062     {
1063       if (csqFields.length <= alleleNumberFieldIndex)
1064       {
1065         return false;
1066       }
1067       String alleleNum = csqFields[alleleNumberFieldIndex];
1068       return String.valueOf(altAlleleIndex + 1).equals(alleleNum);
1069     }
1070
1071     /*
1072      * else consequence allele must match variant allele
1073      */
1074     if (alleleFieldIndex > -1 && csqFields.length > alleleFieldIndex)
1075     {
1076       String csqAllele = csqFields[alleleFieldIndex];
1077       String vcfAllele = variant.getAlternateAllele(altAlleleIndex)
1078               .getBaseString();
1079       return csqAllele.equals(vcfAllele);
1080     }
1081     return false;
1082   }
1083
1084   /**
1085    * Add any allele-specific VCF key-value data to the sequence feature
1086    * 
1087    * @param variant
1088    * @param seq
1089    * @param sf
1090    * @param altAlelleIndex
1091    *          (0, 1..)
1092    */
1093   protected void addAlleleProperties(VariantContext variant, SequenceI seq,
1094           SequenceFeature sf, final int altAlelleIndex)
1095   {
1096     Map<String, Object> atts = variant.getAttributes();
1097
1098     for (Entry<String, Object> att : atts.entrySet())
1099     {
1100       String key = att.getKey();
1101
1102       /*
1103        * extract Consequence data (if present) that we are able to
1104        * associated with the allele for this variant feature
1105        */
1106       if (CSQ_FIELD.equals(key))
1107       {
1108         addConsequences(variant, seq, sf, altAlelleIndex);
1109         continue;
1110       }
1111
1112       /*
1113        * filter out fields we don't want to capture
1114        */
1115       if (!vcfFieldsOfInterest.contains(key))
1116       {
1117         continue;
1118       }
1119
1120       /*
1121        * we extract values for other data which are allele-specific; 
1122        * these may be per alternate allele (INFO[key].Number = 'A') 
1123        * or per allele including reference (INFO[key].Number = 'R') 
1124        */
1125       VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(key);
1126       if (infoHeader == null)
1127       {
1128         /*
1129          * can't be sure what data belongs to this allele, so
1130          * play safe and don't take any
1131          */
1132         continue;
1133       }
1134
1135       VCFHeaderLineCount number = infoHeader.getCountType();
1136       int index = altAlelleIndex;
1137       if (number == VCFHeaderLineCount.R)
1138       {
1139         /*
1140          * one value per allele including reference, so bump index
1141          * e.g. the 3rd value is for the  2nd alternate allele
1142          */
1143         index++;
1144       }
1145       else if (number != VCFHeaderLineCount.A)
1146       {
1147         /*
1148          * don't save other values as not allele-related
1149          */
1150         continue;
1151       }
1152
1153       /*
1154        * take the index'th value
1155        */
1156       String value = getAttributeValue(variant, key, index);
1157       if (value != null)
1158       {
1159         sf.setValue(key, value);
1160       }
1161     }
1162   }
1163
1164   /**
1165    * Inspects CSQ data blocks (consequences) and adds attributes on the sequence
1166    * feature for the current allele (and transcript if applicable)
1167    * <p>
1168    * Allele matching: if field ALLELE_NUM is present, it must match
1169    * altAlleleIndex. If not present, then field Allele value must match the VCF
1170    * Allele.
1171    * <p>
1172    * Transcript matching: if sequence name can be identified to at least one of
1173    * the consequences' Feature values, then select only consequences that match
1174    * the value (i.e. consequences for the current transcript sequence). If not,
1175    * take all consequences (this is the case when adding features to the gene
1176    * sequence).
1177    * 
1178    * @param variant
1179    * @param seq
1180    * @param sf
1181    * @param altAlleleIndex
1182    *          (0, 1..)
1183    */
1184   protected void addConsequences(VariantContext variant, SequenceI seq,
1185           SequenceFeature sf, int altAlleleIndex)
1186   {
1187     /*
1188      * first try to identify the matching consequence
1189      */
1190     String myConsequence = getConsequenceForAlleleAndFeature(variant,
1191             CSQ_FIELD, altAlleleIndex, csqAlleleFieldIndex,
1192             csqAlleleNumberFieldIndex, seq.getName().toLowerCase(),
1193             csqFeatureFieldIndex);
1194
1195     Object value = variant.getAttribute(CSQ_FIELD);
1196
1197     if (value == null || !(value instanceof List<?>))
1198     {
1199       return;
1200     }
1201
1202     List<String> consequences = (List<String>) value;
1203
1204     /*
1205      * inspect CSQ consequences; restrict to the consequence
1206      * associated with the current transcript (Feature)
1207      */
1208     Map<String, String> csqValues = new HashMap<>();
1209
1210     for (String consequence : consequences)
1211     {
1212       if (myConsequence == null || myConsequence.equals(consequence))
1213       {
1214         String[] csqFields = consequence.split(PIPE_REGEX);
1215
1216         /*
1217          * inspect individual fields of this consequence, copying non-null
1218          * values which are 'fields of interest'
1219          */
1220         int i = 0;
1221         for (String field : csqFields)
1222         {
1223           if (field != null && field.length() > 0)
1224           {
1225             String id = vepFieldsOfInterest.get(i);
1226             if (id != null)
1227             {
1228               csqValues.put(id, field);
1229             }
1230           }
1231           i++;
1232         }
1233       }
1234     }
1235
1236     if (!csqValues.isEmpty())
1237     {
1238       sf.setValue(CSQ_FIELD, csqValues);
1239     }
1240   }
1241
1242   /**
1243    * A convenience method to complement a dna base and return the string value
1244    * of its complement
1245    * 
1246    * @param reference
1247    * @return
1248    */
1249   protected String complement(byte[] reference)
1250   {
1251     return String.valueOf(Dna.getComplement((char) reference[0]));
1252   }
1253
1254   /**
1255    * Determines the location of the query range (chromosome positions) in a
1256    * different reference assembly.
1257    * <p>
1258    * If the range is just a subregion of one for which we already have a mapping
1259    * (for example, an exon sub-region of a gene), then the mapping is just
1260    * computed arithmetically.
1261    * <p>
1262    * Otherwise, calls the Ensembl REST service that maps from one assembly
1263    * reference's coordinates to another's
1264    * 
1265    * @param queryRange
1266    *          start-end chromosomal range in 'fromRef' coordinates
1267    * @param chromosome
1268    * @param species
1269    * @param fromRef
1270    *          assembly reference for the query coordinates
1271    * @param toRef
1272    *          assembly reference we wish to translate to
1273    * @return the start-end range in 'toRef' coordinates
1274    */
1275   protected int[] mapReferenceRange(int[] queryRange, String chromosome,
1276           String species, String fromRef, String toRef)
1277   {
1278     /*
1279      * first try shorcut of computing the mapping as a subregion of one
1280      * we already have (e.g. for an exon, if we have the gene mapping)
1281      */
1282     int[] mappedRange = findSubsumedRangeMapping(queryRange, chromosome,
1283             species, fromRef, toRef);
1284     if (mappedRange != null)
1285     {
1286       return mappedRange;
1287     }
1288
1289     /*
1290      * call (e.g.) http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37
1291      */
1292     EnsemblMap mapper = new EnsemblMap();
1293     int[] mapping = mapper.getAssemblyMapping(species, chromosome, fromRef,
1294             toRef, queryRange);
1295
1296     if (mapping == null)
1297     {
1298       // mapping service failure
1299       return null;
1300     }
1301
1302     /*
1303      * save mapping for possible future re-use
1304      */
1305     String key = makeRangesKey(chromosome, species, fromRef, toRef);
1306     if (!assemblyMappings.containsKey(key))
1307     {
1308       assemblyMappings.put(key, new HashMap<int[], int[]>());
1309     }
1310
1311     assemblyMappings.get(key).put(queryRange, mapping);
1312
1313     return mapping;
1314   }
1315
1316   /**
1317    * If we already have a 1:1 contiguous mapping which subsumes the given query
1318    * range, this method just calculates and returns the subset of that mapping,
1319    * else it returns null. In practical terms, if a gene has a contiguous
1320    * mapping between (for example) GRCh37 and GRCh38, then we assume that its
1321    * subsidiary exons occupy unchanged relative positions, and just compute
1322    * these as offsets, rather than do another lookup of the mapping.
1323    * <p>
1324    * If in future these assumptions prove invalid (e.g. for bacterial dna?!),
1325    * simply remove this method or let it always return null.
1326    * <p>
1327    * Warning: many rapid calls to the /map service map result in a 429 overload
1328    * error response
1329    * 
1330    * @param queryRange
1331    * @param chromosome
1332    * @param species
1333    * @param fromRef
1334    * @param toRef
1335    * @return
1336    */
1337   protected int[] findSubsumedRangeMapping(int[] queryRange, String chromosome,
1338           String species, String fromRef, String toRef)
1339   {
1340     String key = makeRangesKey(chromosome, species, fromRef, toRef);
1341     if (assemblyMappings.containsKey(key))
1342     {
1343       Map<int[], int[]> mappedRanges = assemblyMappings.get(key);
1344       for (Entry<int[], int[]> mappedRange : mappedRanges.entrySet())
1345       {
1346         int[] fromRange = mappedRange.getKey();
1347         int[] toRange = mappedRange.getValue();
1348         if (fromRange[1] - fromRange[0] == toRange[1] - toRange[0])
1349         {
1350           /*
1351            * mapping is 1:1 in length, so we trust it to have no discontinuities
1352            */
1353           if (MappingUtils.rangeContains(fromRange, queryRange))
1354           {
1355             /*
1356              * fromRange subsumes our query range
1357              */
1358             int offset = queryRange[0] - fromRange[0];
1359             int mappedRangeFrom = toRange[0] + offset;
1360             int mappedRangeTo = mappedRangeFrom + (queryRange[1] - queryRange[0]);
1361             return new int[] { mappedRangeFrom, mappedRangeTo };
1362           }
1363         }
1364       }
1365     }
1366     return null;
1367   }
1368
1369   /**
1370    * Transfers the sequence feature to the target sequence, locating its start
1371    * and end range based on the mapping. Features which do not overlap the
1372    * target sequence are ignored.
1373    * 
1374    * @param sf
1375    * @param targetSequence
1376    * @param mapping
1377    *          mapping from the feature's coordinates to the target sequence
1378    */
1379   protected void transferFeature(SequenceFeature sf,
1380           SequenceI targetSequence, MapList mapping)
1381   {
1382     int[] mappedRange = mapping.locateInTo(sf.getBegin(), sf.getEnd());
1383   
1384     if (mappedRange != null)
1385     {
1386       String group = sf.getFeatureGroup();
1387       int newBegin = Math.min(mappedRange[0], mappedRange[1]);
1388       int newEnd = Math.max(mappedRange[0], mappedRange[1]);
1389       SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
1390               group, sf.getScore());
1391       targetSequence.addSequenceFeature(copy);
1392     }
1393   }
1394
1395   /**
1396    * Formats a ranges map lookup key
1397    * 
1398    * @param chromosome
1399    * @param species
1400    * @param fromRef
1401    * @param toRef
1402    * @return
1403    */
1404   protected static String makeRangesKey(String chromosome, String species,
1405           String fromRef, String toRef)
1406   {
1407     return species + EXCL + chromosome + EXCL + fromRef + EXCL
1408             + toRef;
1409   }
1410 }