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