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