Merge branch 'develop' into features/JAL-1793VCF
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 28 Sep 2017 12:34:10 +0000 (13:34 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 28 Sep 2017 12:34:10 +0000 (13:34 +0100)
24 files changed:
resources/lang/Messages.properties
src/jalview/analysis/AlignmentUtils.java
src/jalview/datamodel/DBRefEntry.java
src/jalview/datamodel/GeneLociI.java [new file with mode: 0644]
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/SequenceI.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/ensembl/EnsemblMap.java [new file with mode: 0644]
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/htsjdk/VCFReader.java [new file with mode: 0644]
src/jalview/gui/AlignFrame.java
src/jalview/gui/IdPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SeqPanel.java
src/jalview/io/gff/Gff3Helper.java
src/jalview/io/vcf/VCFLoader.java [new file with mode: 0644]
src/jalview/jbgui/GAlignFrame.java
src/jalview/util/MappingUtils.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/ext/htsjdk/VCFReaderTest.java [new file with mode: 0644]
test/jalview/gui/PopupMenuTest.java
test/jalview/io/vcf/VCFLoaderTest.java [new file with mode: 0644]
test/jalview/util/MappingUtilsTest.java

index 5d9bdff..5620386 100644 (file)
@@ -490,6 +490,8 @@ label.settings_for_type = Settings for {0}
 label.view_full_application = View in Full Application
 label.load_associated_tree = Load Associated Tree...
 label.load_features_annotations = Load Features/Annotations...
+label.load_vcf = Load SNP variants from plain text or indexed VCF data
+label.load_vcf_file = Load VCF File
 label.export_features = Export Features...
 label.export_annotations = Export Annotations...
 label.to_upper_case = To Upper Case
@@ -1319,3 +1321,4 @@ label.select_hidden_colour = Select hidden colour
 label.overview = Overview
 label.reset_to_defaults = Reset to defaults
 label.oview_calc = Recalculating overview...
+label.feature_details = Feature details
\ No newline at end of file
index 2b9b9f9..c88a462 100644 (file)
@@ -36,6 +36,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.SequenceFeatures;
+import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
@@ -105,6 +106,15 @@ public class AlignmentUtils
     {
       return variant == null ? null : variant.getFeatureGroup();
     }
+
+    /**
+     * toString for aid in the debugger only
+     */
+    @Override
+    public String toString()
+    {
+      return base + ":" + (variant == null ? "" : variant.getDescription());
+    }
   }
 
   /**
@@ -1623,7 +1633,7 @@ public class AlignmentUtils
           AlignmentI dataset, SequenceI[] products)
   {
     if (dataset == null || dataset.getDataset() != null)
-    {
+  {
       throw new IllegalArgumentException(
               "IMPLEMENTATION ERROR: dataset.getDataset() must be null!");
     }
@@ -1635,10 +1645,10 @@ public class AlignmentUtils
     {
       productSeqs = new HashSet<SequenceI>();
       for (SequenceI seq : products)
-      {
-        productSeqs.add(seq.getDatasetSequence() == null ? seq
-                : seq.getDatasetSequence());
-      }
+    {
+        productSeqs.add(seq.getDatasetSequence() == null ? seq : seq
+                .getDatasetSequence());
+    }
     }
 
     /*
@@ -1660,15 +1670,15 @@ public class AlignmentUtils
       List<AlignedCodonFrame> seqMappings = MappingUtils
               .findMappingsForSequence(dnaSeq, mappings);
       for (AlignedCodonFrame mapping : seqMappings)
-      {
+    {
         List<Mapping> mappingsFromSequence = mapping
                 .getMappingsFromSequence(dnaSeq);
 
         for (Mapping aMapping : mappingsFromSequence)
-        {
+      {
           MapList mapList = aMapping.getMap();
           if (mapList.getFromRatio() == 1)
-          {
+        {
             /*
              * not a dna-to-protein mapping (likely dna-to-cds)
              */
@@ -1694,15 +1704,15 @@ public class AlignmentUtils
           if (cdsSeq != null)
           {
             if (!foundSeqs.contains(cdsSeq))
-            {
+          {
               foundSeqs.add(cdsSeq);
               SequenceI derivedSequence = cdsSeq.deriveSequence();
               cdsSeqs.add(derivedSequence);
               if (!dataset.getSequences().contains(cdsSeq))
-              {
+            {
                 dataset.addSequence(cdsSeq);
-              }
             }
+          }
             continue;
           }
 
@@ -1730,9 +1740,8 @@ public class AlignmentUtils
           /*
            * add a mapping from CDS to the (unchanged) mapped to range
            */
-          List<int[]> cdsRange = Collections
-                  .singletonList(new int[]
-                  { 1, cdsSeq.getLength() });
+          List<int[]> cdsRange = Collections.singletonList(new int[] { 1,
+              cdsSeq.getLength() });
           MapList cdsToProteinMap = new MapList(cdsRange,
                   mapList.getToRanges(), mapList.getFromRatio(),
                   mapList.getToRatio());
@@ -1782,40 +1791,46 @@ public class AlignmentUtils
 
           for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs())
           {
-            // creates a complementary cross-reference to the source sequence's
-            // primary reference.
-
-            DBRefEntry cdsCrossRef = new DBRefEntry(primRef.getSource(),
-                    primRef.getSource() + ":" + primRef.getVersion(),
-                    primRef.getAccessionId());
-            cdsCrossRef
-                    .setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap)));
+            /*
+             * create a cross-reference from CDS to the source sequence's
+             * primary reference and vice versa
+             */
+
+            String source = primRef.getSource();
+            String version = primRef.getVersion();
+            DBRefEntry cdsCrossRef = new DBRefEntry(source, source + ":"
+                    + version, primRef.getAccessionId());
+            cdsCrossRef.setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap
+                    .getInverse())));
             cdsSeqDss.addDBRef(cdsCrossRef);
 
+            dnaSeq.addDBRef(new DBRefEntry(source, version, cdsSeq
+                    .getName(), new Mapping(cdsSeqDss, dnaToCdsMap)));
+
             // problem here is that the cross-reference is synthesized -
             // cdsSeq.getName() may be like 'CDS|dnaaccession' or
             // 'CDS|emblcdsacc'
             // assuming cds version same as dna ?!?
 
-            DBRefEntry proteinToCdsRef = new DBRefEntry(primRef.getSource(),
-                    primRef.getVersion(), cdsSeq.getName());
+            DBRefEntry proteinToCdsRef = new DBRefEntry(source, version,
+                    cdsSeq.getName());
             //
-            proteinToCdsRef.setMap(
-                    new Mapping(cdsSeqDss, cdsToProteinMap.getInverse()));
+            proteinToCdsRef.setMap(new Mapping(cdsSeqDss, cdsToProteinMap
+                    .getInverse()));
             proteinProduct.addDBRef(proteinToCdsRef);
-          }
+        }
 
           /*
            * transfer any features on dna that overlap the CDS
            */
           transferFeatures(dnaSeq, cdsSeq, dnaToCdsMap, null,
                   SequenceOntologyI.CDS);
-        }
       }
     }
+    }
 
-    AlignmentI cds = new Alignment(
-            cdsSeqs.toArray(new SequenceI[cdsSeqs.size()]));
+    AlignmentI cds = new Alignment(cdsSeqs.toArray(new SequenceI[cdsSeqs
+            .size()]));
     cds.setDataset(dataset);
 
     return cds;
@@ -2365,7 +2380,7 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
@@ -2387,7 +2402,7 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
@@ -2409,7 +2424,7 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
@@ -2542,6 +2557,22 @@ public class AlignmentUtils
         // not handling multi-locus variant features
         continue;
       }
+
+      /*
+       * extract dna variants to a string array
+       */
+      String alls = (String) sf.getValue(Gff3Helper.ALLELES);
+      if (alls == null)
+      {
+        continue; // non-SNP VCF variant perhaps - can't process this
+      }
+      String[] alleles = alls.toUpperCase().split(",");
+      int i = 0;
+      for (String allele : alleles)
+      {
+        alleles[i++] = allele.trim(); // lose any space characters "A, G"
+      }
+
       int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
       if (mapsTo == null)
       {
@@ -2560,21 +2591,6 @@ public class AlignmentUtils
       }
 
       /*
-       * extract dna variants to a string array
-       */
-      String alls = (String) sf.getValue("alleles");
-      if (alls == null)
-      {
-        continue;
-      }
-      String[] alleles = alls.toUpperCase().split(",");
-      int i = 0;
-      for (String allele : alleles)
-      {
-        alleles[i++] = allele.trim(); // lose any space characters "A, G"
-      }
-
-      /*
        * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
        */
       int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
index f7837f7..98868ce 100755 (executable)
@@ -27,7 +27,20 @@ import java.util.List;
 
 public class DBRefEntry implements DBRefEntryI
 {
-  String source = "", version = "", accessionId = "";
+  /*
+   * the mapping to chromosome (genome) is held as an instance with
+   * source = speciesId
+   * version = assemblyId
+   * accessionId = "chromosome:" + chromosomeId
+   * map = mapping from sequence to reference assembly
+   */
+  public static final String CHROMOSOME = "chromosome";
+
+  String source = "";
+
+  String version = "";
+
+  String accessionId = "";
 
   /**
    * maps from associated sequence to the database sequence's coordinate system
@@ -331,4 +344,14 @@ public class DBRefEntry implements DBRefEntryI
     }
     return true;
   }
+
+  /**
+   * Mappings to chromosome are held with accessionId as "chromosome:id"
+   * 
+   * @return
+   */
+  public boolean isChromosome()
+  {
+    return accessionId != null && accessionId.startsWith(CHROMOSOME + ":");
+  }
 }
diff --git a/src/jalview/datamodel/GeneLociI.java b/src/jalview/datamodel/GeneLociI.java
new file mode 100644 (file)
index 0000000..f8c7ec5
--- /dev/null
@@ -0,0 +1,38 @@
+package jalview.datamodel;
+
+import jalview.util.MapList;
+
+/**
+ * An interface to model one or more contiguous regions on one chromosome
+ */
+public interface GeneLociI
+{
+  /**
+   * Answers the species identifier
+   * 
+   * @return
+   */
+  String getSpeciesId();
+
+  /**
+   * Answers the reference assembly identifier
+   * 
+   * @return
+   */
+  String getAssemblyId();
+
+  /**
+   * Answers the chromosome identifier e.g. "2", "Y", "II"
+   * 
+   * @return
+   */
+  String getChromosomeId();
+
+  /**
+   * Answers the mapping from sequence to chromosome loci. For a reverse strand
+   * mapping, the chromosomal ranges will have start > end.
+   * 
+   * @return
+   */
+  MapList getMap();
+}
index 2f1da7f..cd743d1 100755 (executable)
@@ -645,10 +645,10 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
-   * DOCUMENT ME!
+   * Sets the sequence description, and also parses out any special formats of
+   * interest
    * 
    * @param desc
-   *          DOCUMENT ME!
    */
   @Override
   public void setDescription(String desc)
@@ -656,10 +656,67 @@ public class Sequence extends ASequence implements SequenceI
     this.description = desc;
   }
 
+  @Override
+  public void setGeneLoci(String speciesId, String assemblyId,
+          String chromosomeId, MapList map)
+  {
+    addDBRef(new DBRefEntry(speciesId, assemblyId, DBRefEntry.CHROMOSOME
+            + ":" + chromosomeId, new Mapping(map)));
+  }
+
   /**
-   * DOCUMENT ME!
+   * Returns the gene loci mapping for the sequence (may be null)
    * 
-   * @return DOCUMENT ME!
+   * @return
+   */
+  @Override
+  public GeneLociI getGeneLoci()
+  {
+    DBRefEntry[] refs = getDBRefs();
+    if (refs != null)
+    {
+      for (final DBRefEntry ref : refs)
+      {
+        if (ref.isChromosome())
+        {
+          return new GeneLociI()
+          {
+            @Override
+            public String getSpeciesId()
+            {
+              return ref.getSource();
+            }
+
+            @Override
+            public String getAssemblyId()
+            {
+              return ref.getVersion();
+            }
+
+            @Override
+            public String getChromosomeId()
+            {
+              // strip of "chromosome:" prefix to chrId
+              return ref.getAccessionId().substring(
+                      DBRefEntry.CHROMOSOME.length() + 1);
+            }
+
+            @Override
+            public MapList getMap()
+            {
+              return ref.getMap().getMap();
+            }
+          };
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Answers the description
+   * 
+   * @return
    */
   @Override
   public String getDescription()
index 9c4087e..6834341 100755 (executable)
@@ -25,6 +25,7 @@ import jalview.datamodel.features.FeatureLocationI;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 import java.util.Vector;
 
 /**
@@ -52,6 +53,16 @@ public class SequenceFeature implements FeatureLocationI
   private static final String LOCATION = "!Location";
 
   /*
+   * map of otherDetails special keys, and their value fields' delimiter
+   */
+  private static final Map<String, String> INFO_KEYS = new HashMap<>();
+
+  static
+  {
+    INFO_KEYS.put("CSQ", ",");
+  }
+
+  /*
    * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
    * name1=value1;name2=value2,value3;...etc
    */
@@ -535,4 +546,58 @@ public class SequenceFeature implements FeatureLocationI
   {
     return begin == 0 && end == 0;
   }
+
+  /**
+   * Answers a formatted text report of feature details
+   * 
+   * @return
+   */
+  public String getDetailsReport()
+  {
+    StringBuilder sb = new StringBuilder(128);
+    if (begin == end)
+    {
+      sb.append(String.format("%s %d %s", type, begin, description));
+    }
+    else
+    {
+      sb.append(String.format("%s %d-%d %s", type, begin, end, description));
+    }
+    if (featureGroup != null)
+    {
+      sb.append(" (").append(featureGroup).append(")");
+    }
+    sb.append("\n\n");
+
+    if (otherDetails != null)
+    {
+      TreeMap<String, Object> ordered = new TreeMap<>(
+              String.CASE_INSENSITIVE_ORDER);
+      ordered.putAll(otherDetails);
+
+      for (Entry<String, Object> entry : ordered.entrySet())
+      {
+        String key = entry.getKey();
+        if (ATTRIBUTES.equals(key))
+        {
+          continue; // to avoid double reporting
+        }
+        if (INFO_KEYS.containsKey(key))
+        {
+          /*
+           * split selected INFO data by delimiter over multiple lines
+           */
+          sb.append(key).append("\n");
+          String delimiter = INFO_KEYS.get(key);
+          sb.append(entry.getValue().toString().replace(delimiter, "\n"));
+        }
+        else
+        {
+          sb.append(key + "=" + entry.getValue().toString() + "\n");
+        }
+      }
+    }
+    String text = sb.toString();
+    return text;
+  }
 }
index 6e6d1aa..03fc545 100755 (executable)
@@ -21,6 +21,7 @@
 package jalview.datamodel;
 
 import jalview.datamodel.features.SequenceFeaturesI;
+import jalview.util.MapList;
 
 import java.util.BitSet;
 import java.util.List;
@@ -534,4 +535,9 @@ public interface SequenceI extends ASequenceI
    * @param c2
    */
   public int replace(char c1, char c2);
+
+  GeneLociI getGeneLoci();
+
+  void setGeneLoci(String speciesId, String assemblyId,
+          String chromosomeId, MapList map);
 }
index 365c1c2..bc914e5 100644 (file)
@@ -23,6 +23,8 @@ package jalview.ext.ensembl;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsModelI;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -144,6 +146,9 @@ public class EnsemblGene extends EnsemblSeqProxy
       {
         continue;
       }
+      
+      parseChromosomeLocations(geneAlignment);
+      
       if (geneAlignment.getHeight() == 1)
       {
         getTranscripts(geneAlignment, geneId);
@@ -161,6 +166,45 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
+   * Parses and saves fields of an Ensembl-style description e.g.
+   * chromosome:GRCh38:17:45051610:45109016:1
+   * 
+   * @param alignment
+   */
+  private void parseChromosomeLocations(AlignmentI alignment)
+  {
+    for (SequenceI seq : alignment.getSequences())
+    {
+      String description = seq.getDescription();
+      if (description == null)
+      {
+        continue;
+      }
+      String[] tokens = description.split(":");
+      if (tokens.length == 6 && tokens[0].startsWith(DBRefEntry.CHROMOSOME))
+      {
+        String ref = tokens[1];
+        String chrom = tokens[2];
+        try
+        {
+          int chStart = Integer.parseInt(tokens[3]);
+          int chEnd = Integer.parseInt(tokens[4]);
+          boolean forwardStrand = "1".equals(tokens[5]);
+          String species = ""; // dunno yet!
+          int[] from = new int[] { seq.getStart(), seq.getEnd() };
+          int[] to = new int[] { forwardStrand ? chStart : chEnd,
+              forwardStrand ? chEnd : chStart };
+          MapList map = new MapList(from, to, 1, 1);
+          seq.setGeneLoci(species, ref, chrom, map);
+        } catch (NumberFormatException e)
+        {
+          System.err.println("Bad integers in description " + description);
+        }
+      }
+    }
+  }
+
+  /**
    * Converts a query, which may contain one or more gene or transcript
    * identifiers, into a non-redundant list of gene identifiers.
    * 
@@ -401,6 +445,8 @@ public class EnsemblGene extends EnsemblSeqProxy
     cdna.transferFeatures(gene.getFeatures().getPositionalFeatures(),
             transcript.getDatasetSequence(), mapping, parentId);
 
+    mapTranscriptToChromosome(transcript, gene, mapping);
+
     /*
      * fetch and save cross-references
      */
@@ -415,6 +461,42 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
+   * If the gene has a mapping to chromosome coordinates, derive the transcript
+   * chromosome regions and save on the transcript sequence
+   * 
+   * @param transcript
+   * @param gene
+   * @param mapping
+   *          the mapping from gene to transcript positions
+   */
+  protected void mapTranscriptToChromosome(SequenceI transcript,
+          SequenceI gene, MapList mapping)
+  {
+    GeneLociI loci = gene.getGeneLoci();
+    if (loci == null)
+    {
+      return;
+    }
+
+    MapList geneMapping = loci.getMap();
+
+    List<int[]> exons = mapping.getFromRanges();
+    List<int[]> transcriptLoci = new ArrayList<>();
+
+    for (int[] exon : exons)
+    {
+      transcriptLoci.add(geneMapping.locateInTo(exon[0], exon[1]));
+    }
+
+    List<int[]> transcriptRange = Arrays.asList(new int[] {
+        transcript.getStart(), transcript.getEnd() });
+    MapList mapList = new MapList(transcriptRange, transcriptLoci, 1, 1);
+
+    transcript.setGeneLoci(loci.getSpeciesId(), loci.getAssemblyId(),
+            loci.getChromosomeId(), mapList);
+  }
+
+  /**
    * Returns the 'transcript_id' property of the sequence feature (or null)
    * 
    * @param feature
diff --git a/src/jalview/ext/ensembl/EnsemblMap.java b/src/jalview/ext/ensembl/EnsemblMap.java
new file mode 100644 (file)
index 0000000..05cc897
--- /dev/null
@@ -0,0 +1,183 @@
+package jalview.ext.ensembl;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefSource;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.List;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+public class EnsemblMap extends EnsemblRestClient
+{
+
+  /**
+   * Default constructor (to use rest.ensembl.org)
+   */
+  public EnsemblMap()
+  {
+    super();
+  }
+
+  /**
+   * Constructor given the target domain to fetch data from
+   * 
+   * @param
+   */
+  public EnsemblMap(String domain)
+  {
+    super(domain);
+  }
+
+  @Override
+  public String getDbName()
+  {
+    return DBRefSource.ENSEMBL;
+  }
+
+  @Override
+  public AlignmentI getSequenceRecords(String queries) throws Exception
+  {
+    return null; // not used
+  }
+
+  /**
+   * Constructs a URL of the format <code>
+   * http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37?content-type=application/json
+   * </code>
+   * 
+   * @param species
+   * @param chromosome
+   * @param fromRef
+   * @param toRef
+   * @param startPos
+   * @param endPos
+   * @return
+   * @throws MalformedURLException
+   */
+  protected URL getUrl(String species, String chromosome, String fromRef,
+          String toRef, int startPos, int endPos)
+          throws MalformedURLException
+  {
+    /*
+     * start-end might be reverse strand - present forwards to the service
+     */
+    boolean forward = startPos <= endPos;
+    int start = forward ? startPos : endPos;
+    int end = forward ? endPos : startPos;
+    String strand = forward ? "1" : "-1";
+    String url = String.format(
+            "%s/map/%s/%s/%s:%d..%d:%s/%s?content-type=application/json",
+            getDomain(), species, fromRef, chromosome, start, end, strand,
+            toRef);
+    try
+    {
+      return new URL(url);
+    } catch (MalformedURLException e)
+    {
+      return null;
+    }
+  }
+
+  @Override
+  protected boolean useGetRequest()
+  {
+    return true;
+  }
+
+  @Override
+  protected String getRequestMimeType(boolean multipleIds)
+  {
+    return "application/json";
+  }
+
+  @Override
+  protected String getResponseMimeType()
+  {
+    return "application/json";
+  }
+
+  @Override
+  protected URL getUrl(List<String> ids) throws MalformedURLException
+  {
+    return null; // not used
+  }
+
+  public int[] getMapping(String species, String chromosome,
+          String fromRef, String toRef, int[] queryRange)
+  {
+    URL url = null;
+    BufferedReader br = null;
+
+    try
+    {
+      url = getUrl(species, chromosome, fromRef, toRef, queryRange[0],
+              queryRange[1]);
+      // System.out.println("Calling " + url);
+      br = getHttpResponse(url, null);
+      return (parseResponse(br));
+    } catch (Throwable t)
+    {
+      System.out.println("Error calling " + url + ": " + t.getMessage());
+      return null;
+    }
+  }
+
+  /**
+   * Parses the JSON response from the /map REST service. The format is (with
+   * some fields omitted)
+   * 
+   * <pre>
+   *  {"mappings": 
+   *    [{
+   *       "original": {"end":45109016,"start":45051610},
+   *       "mapped"  : {"end":43186384,"start":43128978} 
+   *  }] }
+   * </pre>
+   * 
+   * @param br
+   * @return
+   */
+  protected int[] parseResponse(BufferedReader br)
+  {
+    int[] result = null;
+    JSONParser jp = new JSONParser();
+
+    try
+    {
+      JSONObject parsed = (JSONObject) jp.parse(br);
+      JSONArray mappings = (JSONArray) parsed.get("mappings");
+
+      Iterator rvals = mappings.iterator();
+      while (rvals.hasNext())
+      {
+        // todo check for "mapped"
+        JSONObject val = (JSONObject) rvals.next();
+        JSONObject mapped = (JSONObject) val.get("mapped");
+        int start = Integer.parseInt(mapped.get("start").toString());
+        int end = Integer.parseInt(mapped.get("end").toString());
+        String strand = mapped.get("strand").toString();
+        if ("1".equals(strand))
+        {
+          result = new int[] { start, end };
+        }
+        else
+        {
+          result = new int[] { end, start };
+        }
+      }
+    } catch (IOException | ParseException | NumberFormatException e)
+    {
+      // ignore
+    }
+    return result;
+  }
+
+}
index 577111e..35ceea3 100644 (file)
@@ -34,6 +34,7 @@ import jalview.datamodel.features.SequenceFeatures;
 import jalview.exceptions.JalviewException;
 import jalview.io.FastaFile;
 import jalview.io.FileParse;
+import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.util.Comparison;
@@ -59,8 +60,6 @@ import java.util.Map.Entry;
  */
 public abstract class EnsemblSeqProxy extends EnsemblRestClient
 {
-  private static final String ALLELES = "alleles";
-
   protected static final String PARENT = "Parent";
 
   protected static final String ID = "ID";
@@ -717,7 +716,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
    */
   static void reverseComplementAlleles(SequenceFeature sf)
   {
-    final String alleles = (String) sf.getValue(ALLELES);
+    final String alleles = (String) sf.getValue(Gff3Helper.ALLELES);
     if (alleles == null)
     {
       return;
@@ -728,7 +727,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       reverseComplementAllele(complement, allele);
     }
     String comp = complement.toString();
-    sf.setValue(ALLELES, comp);
+    sf.setValue(Gff3Helper.ALLELES, comp);
     sf.setDescription(comp);
 
     /*
@@ -738,7 +737,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     String atts = sf.getAttributes();
     if (atts != null)
     {
-      atts = atts.replace(ALLELES + "=" + alleles, ALLELES + "=" + comp);
+      atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles,
+              Gff3Helper.ALLELES + "=" + comp);
       sf.setAttributes(atts);
     }
   }
diff --git a/src/jalview/ext/htsjdk/VCFReader.java b/src/jalview/ext/htsjdk/VCFReader.java
new file mode 100644 (file)
index 0000000..14c057f
--- /dev/null
@@ -0,0 +1,214 @@
+package jalview.ext.htsjdk;
+
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFFileReader;
+import htsjdk.variant.vcf.VCFHeader;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A thin wrapper for htsjdk classes to read either plain, or compressed, or
+ * compressed and indexed VCF files
+ */
+public class VCFReader implements Closeable, Iterable<VariantContext>
+{
+  private static final String GZ = "gz";
+
+  private static final String TBI_EXTENSION = ".tbi";
+
+  private boolean indexed;
+
+  private VCFFileReader reader;
+
+  /**
+   * Constructor given a raw or compressed VCF file or a (tabix) index file
+   * <p>
+   * For now, file type is inferred from its suffix: .gz or .bgz for compressed
+   * data, .tbi for an index file, anything else is assumed to be plain text
+   * VCF.
+   * 
+   * @param f
+   * @throws IOException
+   */
+  public VCFReader(String filePath) throws IOException
+  {
+    if (filePath.endsWith(GZ))
+    {
+      if (new File(filePath + TBI_EXTENSION).exists())
+      {
+        indexed = true;
+      }
+    }
+    else if (filePath.endsWith(TBI_EXTENSION))
+    {
+      indexed = true;
+      filePath = filePath.substring(0, filePath.length() - 4);
+    }
+
+    reader = new VCFFileReader(new File(filePath), indexed);
+  }
+
+  @Override
+  public void close() throws IOException
+  {
+    if (reader != null)
+    {
+      reader.close();
+    }
+  }
+
+  /**
+   * Returns an iterator over VCF variants in the file. The client should call
+   * close() on the iterator when finished with it.
+   */
+  @Override
+  public CloseableIterator<VariantContext> iterator()
+  {
+    return reader == null ? null : reader.iterator();
+  }
+
+  /**
+   * Queries for records overlapping the region specified. Note that this method
+   * is performant if the VCF file is indexed, and may be very slow if it is
+   * not.
+   * <p>
+   * Client code should call close() on the iterator when finished with it.
+   * 
+   * @param chrom
+   *          the chromosome to query
+   * @param start
+   *          query interval start
+   * @param end
+   *          query interval end
+   * @return
+   */
+  public CloseableIterator<VariantContext> query(final String chrom,
+          final int start, final int end)
+  {
+   if (reader == null) {
+     return null;
+   }
+    if (indexed)
+    {
+      return reader.query(chrom, start, end);
+    }
+    else
+    {
+      return queryUnindexed(chrom, start, end);
+    }
+  }
+
+  /**
+   * Returns an iterator over variant records read from a flat file which
+   * overlap the specified chromosomal positions. Call close() on the iterator
+   * when finished with it!
+   * 
+   * @param chrom
+   * @param start
+   * @param end
+   * @return
+   */
+  protected CloseableIterator<VariantContext> queryUnindexed(
+          final String chrom, final int start, final int end)
+  {
+    final CloseableIterator<VariantContext> it = reader.iterator();
+    
+    return new CloseableIterator<VariantContext>()
+    {
+      boolean atEnd = false;
+
+      // prime look-ahead buffer with next matching record
+      private VariantContext next = findNext();
+
+      private VariantContext findNext()
+      {
+        if (atEnd)
+        {
+          return null;
+        }
+        VariantContext variant = null;
+        while (it.hasNext())
+        {
+          variant = it.next();
+          int vstart = variant.getStart();
+
+          if (vstart > end)
+          {
+            atEnd = true;
+            close();
+            return null;
+          }
+
+          int vend = variant.getEnd();
+          // todo what is the undeprecated way to get
+          // the chromosome for the variant?
+          if (chrom.equals(variant.getChr()) && (vstart <= end)
+                  && (vend >= start))
+          {
+            return variant;
+          }
+        }
+        return null;
+      }
+
+      @Override
+      public boolean hasNext()
+      {
+        boolean hasNext = !atEnd && (next != null);
+        if (!hasNext)
+        {
+          close();
+        }
+        return hasNext;
+      }
+
+      @Override
+      public VariantContext next()
+      {
+        /*
+         * return the next match, and then re-prime
+         * it with the following one (if any)
+         */
+        VariantContext temp = next;
+        next = findNext();
+        return temp;
+      }
+
+      @Override
+      public void remove()
+      {
+        // not implemented
+      }
+
+      @Override
+      public void close()
+      {
+        it.close();
+      }
+    };
+  }
+
+  /**
+   * Returns an object that models the VCF file headers
+   * 
+   * @return
+   */
+  public VCFHeader getFileHeader()
+  {
+    return reader == null ? null : reader.getFileHeader();
+  }
+
+  /**
+   * Answers true if we are processing a tab-indexed VCF file, false if it is a
+   * plain text (uncompressed) file.
+   * 
+   * @return
+   */
+  public boolean isIndex()
+  {
+    return indexed;
+  }
+}
index 13b715e..95cabcd 100644 (file)
@@ -81,6 +81,7 @@ import jalview.io.JnetAnnotationMaker;
 import jalview.io.NewickFile;
 import jalview.io.ScoreMatrixFile;
 import jalview.io.TCoffeeScoreFile;
+import jalview.io.vcf.VCFLoader;
 import jalview.jbgui.GAlignFrame;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemes;
@@ -841,6 +842,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     AlignmentI al = getViewport().getAlignment();
     boolean nucleotide = al.isNucleotide();
 
+    loadVcf.setVisible(nucleotide);
     showTranslation.setVisible(nucleotide);
     showReverse.setVisible(nucleotide);
     showReverseComplement.setVisible(nucleotide);
@@ -5584,6 +5586,26 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       new CalculationChooser(AlignFrame.this);
     }
   }
+
+  @Override
+  protected void loadVcf_actionPerformed()
+  {
+    JalviewFileChooser chooser = new JalviewFileChooser(
+            Cache.getProperty("LAST_DIRECTORY"));
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file"));
+    chooser.setToolTipText(MessageManager.getString("label.load_vcf_file"));
+
+    int value = chooser.showOpenDialog(null);
+
+    if (value == JalviewFileChooser.APPROVE_OPTION)
+    {
+      String choice = chooser.getSelectedFile().getPath();
+      Cache.setProperty("LAST_DIRECTORY", choice);
+      new VCFLoader(viewport.getAlignment()).loadVCF(choice, this);
+    }
+
+  }
 }
 
 class PrintThread extends Thread
index 3cc0ed3..f0aefb1 100755 (executable)
@@ -331,7 +331,8 @@ public class IdPanel extends JPanel
      *  and any non-positional features
      */
     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-    for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
+    List<SequenceFeature> features = sq.getFeatures().getNonPositionalFeatures();
+    for (SequenceFeature sf : features)
     {
       if (sf.links != null)
       {
@@ -342,7 +343,7 @@ public class IdPanel extends JPanel
       }
     }
 
-    PopupMenu pop = new PopupMenu(alignPanel, sq, nlinks,
+    PopupMenu pop = new PopupMenu(alignPanel, sq, features,
             Preferences.getGroupURLLinks());
     pop.show(this, e.getX(), e.getY());
   }
index 846ba64..b574ddd 100644 (file)
@@ -34,7 +34,6 @@ import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
-import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -176,25 +175,31 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    * Creates a new PopupMenu object.
    * 
    * @param ap
-   *          DOCUMENT ME!
    * @param seq
-   *          DOCUMENT ME!
+   * @param features
+   *          non-positional features (for seq not null), or positional features
+   *          at residue (for seq equal to null)
    */
-  public PopupMenu(final AlignmentPanel ap, Sequence seq,
-          List<String> links)
+  public PopupMenu(final AlignmentPanel ap, SequenceI seq,
+          List<SequenceFeature> features)
   {
-    this(ap, seq, links, null);
+    this(ap, seq, features, null);
   }
 
   /**
+   * Constructor
    * 
-   * @param ap
+   * @param alignPanel
    * @param seq
-   * @param links
+   *          the sequence under the cursor if in the Id panel, null if in the
+   *          sequence panel
+   * @param features
+   *          non-positional features if in the Id panel, features at the
+   *          clicked residue if in the sequence panel
    * @param groupLinks
    */
-  public PopupMenu(final AlignmentPanel ap, final SequenceI seq,
-          List<String> links, List<String> groupLinks)
+  public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq,
+          List<SequenceFeature> features, List<String> groupLinks)
   {
     // /////////////////////////////////////////////////////////
     // If this is activated from the sequence panel, the user may want to
@@ -202,7 +207,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     //
     // If from the IDPanel, we must display the sequence menu
     // ////////////////////////////////////////////////////////
-    this.ap = ap;
+    this.ap = alignPanel;
     sequence = seq;
 
     for (String ff : FileFormats.getInstance().getWritableFormats(true))
@@ -237,9 +242,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     /*
      * And repeat for the current selection group (if there is one):
      */
-    final List<SequenceI> selectedGroup = (ap.av.getSelectionGroup() == null
+    final List<SequenceI> selectedGroup = (alignPanel.av.getSelectionGroup() == null
             ? Collections.<SequenceI> emptyList()
-            : ap.av.getSelectionGroup().getSequences());
+            : alignPanel.av.getSelectionGroup().getSequences());
     buildAnnotationTypesMenus(groupShowAnnotationsMenu,
             groupHideAnnotationsMenu, selectedGroup);
     configureReferenceAnnotationsMenu(groupAddReferenceAnnotations,
@@ -257,7 +262,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     if (seq != null)
     {
       sequenceMenu.setText(sequence.getName());
-      if (seq == ap.av.getAlignment().getSeqrep())
+      if (seq == alignPanel.av.getAlignment().getSeqrep())
       {
         makeReferenceSeq.setText(
                 MessageManager.getString("action.unmark_as_reference"));
@@ -268,7 +273,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 MessageManager.getString("action.set_as_reference"));
       }
 
-      if (!ap.av.getAlignment().isNucleotide())
+      if (!alignPanel.av.getAlignment().isNucleotide())
       {
         remove(rnaStructureMenu);
       }
@@ -279,7 +284,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
          * add menu items to 2D-render any alignment or sequence secondary
          * structure annotation
          */
-        AlignmentAnnotation[] aas = ap.av.getAlignment()
+        AlignmentAnnotation[] aas = alignPanel.av.getAlignment()
                 .getAlignmentAnnotation();
         if (aas != null)
         {
@@ -299,7 +304,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 @Override
                 public void actionPerformed(ActionEvent e)
                 {
-                  new AppVarna(seq, aa, ap);
+                  new AppVarna(seq, aa, alignPanel);
                 }
               });
               rnaStructureMenu.add(menuItem);
@@ -328,7 +333,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 public void actionPerformed(ActionEvent e)
                 {
                   // TODO: VARNA does'nt print gaps in the sequence
-                  new AppVarna(seq, aa, ap);
+                  new AppVarna(seq, aa, alignPanel);
                 }
               });
               rnaStructureMenu.add(menuItem);
@@ -353,8 +358,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       });
       add(menuItem);
 
-      if (ap.av.getSelectionGroup() != null
-              && ap.av.getSelectionGroup().getSize() > 1)
+      if (alignPanel.av.getSelectionGroup() != null
+              && alignPanel.av.getSelectionGroup().getSize() > 1)
       {
         menuItem = new JMenuItem(MessageManager
                 .formatMessage("label.represent_group_with", new Object[]
@@ -370,12 +375,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         sequenceMenu.add(menuItem);
       }
 
-      if (ap.av.hasHiddenRows())
+      if (alignPanel.av.hasHiddenRows())
       {
-        final int index = ap.av.getAlignment().findIndex(seq);
+        final int index = alignPanel.av.getAlignment().findIndex(seq);
 
-        if (ap.av.adjustForHiddenSeqs(index)
-                - ap.av.adjustForHiddenSeqs(index - 1) > 1)
+        if (alignPanel.av.adjustForHiddenSeqs(index)
+                - alignPanel.av.adjustForHiddenSeqs(index - 1) > 1)
         {
           menuItem = new JMenuItem(
                   MessageManager.getString("action.reveal_sequences"));
@@ -384,10 +389,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
             @Override
             public void actionPerformed(ActionEvent e)
             {
-              ap.av.showSequence(index);
-              if (ap.overviewPanel != null)
+              alignPanel.av.showSequence(index);
+              if (alignPanel.overviewPanel != null)
               {
-                ap.overviewPanel.updateOverviewImage();
+                alignPanel.overviewPanel.updateOverviewImage();
               }
             }
           });
@@ -396,7 +401,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     }
     // for the case when no sequences are even visible
-    if (ap.av.hasHiddenRows())
+    if (alignPanel.av.hasHiddenRows())
     {
       {
         menuItem = new JMenuItem(
@@ -406,10 +411,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
           @Override
           public void actionPerformed(ActionEvent e)
           {
-            ap.av.showAllHiddenSeqs();
-            if (ap.overviewPanel != null)
+            alignPanel.av.showAllHiddenSeqs();
+            if (alignPanel.overviewPanel != null)
             {
-              ap.overviewPanel.updateOverviewImage();
+              alignPanel.overviewPanel.updateOverviewImage();
             }
           }
         });
@@ -418,9 +423,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     }
 
-    SequenceGroup sg = ap.av.getSelectionGroup();
+    SequenceGroup sg = alignPanel.av.getSelectionGroup();
     boolean isDefinedGroup = (sg != null)
-            ? ap.av.getAlignment().getGroups().contains(sg)
+            ? alignPanel.av.getAlignment().getGroups().contains(sg)
             : false;
 
     if (sg != null && sg.getSize() > 0)
@@ -458,7 +463,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       Hashtable<String, PDBEntry> pdbe = new Hashtable<>(), reppdb = new Hashtable<>();
 
       SequenceI sqass = null;
-      for (SequenceI sq : ap.av.getSequenceSelection())
+      for (SequenceI sq : alignPanel.av.getSequenceSelection())
       {
         Vector<PDBEntry> pes = sq.getDatasetSequence().getAllPDBEntries();
         if (pes != null && pes.size() > 0)
@@ -508,24 +513,113 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       rnaStructureMenu.setVisible(false);
     }
 
-    if (links != null && links.size() > 0)
+    addLinks(seq, features);
+
+    if (seq == null)
     {
-      addFeatureLinks(seq, links);
+      addFeatureDetails(features);
     }
   }
 
   /**
+   * Add a link to show feature details for each sequence feature
+   * 
+   * @param features
+   */
+  protected void addFeatureDetails(List<SequenceFeature> features)
+  {
+    if (features.isEmpty())
+    {
+      return;
+    }
+    JMenu details = new JMenu(
+            MessageManager.getString("label.feature_details"));
+    add(details);
+
+    for (final SequenceFeature sf : features)
+    {
+      int start = sf.getBegin();
+      int end = sf.getEnd();
+      String desc = null;
+      if (start == end)
+      {
+        desc = String.format("%s %d", sf.getType(), start);
+      }
+      else
+      {
+        desc = String.format("%s %d-%d", sf.getType(), start, end);
+      }
+      if (sf.getFeatureGroup() != null)
+      {
+        desc = desc + " (" + sf.getFeatureGroup() + ")";
+      }
+      JMenuItem item = new JMenuItem(desc);
+      item.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          showFeatureDetails(sf);
+        }
+      });
+      details.add(item);
+    }
+  }
+
+  /**
+   * Opens a panel showing a text report of feature dteails
+   * 
+   * @param sf
+   */
+  protected void showFeatureDetails(SequenceFeature sf)
+  {
+    CutAndPasteTransfer cap = new CutAndPasteTransfer();
+    cap.setText(sf.getDetailsReport());
+    Desktop.addInternalFrame(cap,
+            MessageManager.getString("label.feature_details"), 500, 500);
+  }
+
+  /**
    * Adds a 'Link' menu item with a sub-menu item for each hyperlink provided.
+   * When seq is not null, these are links for the sequence id, which may be to
+   * external web sites for the sequence accession, and/or links embedded in
+   * non-positional features. When seq is null, only links embedded in the
+   * provided features are added.
    * 
    * @param seq
-   * @param links
+   * @param features
    */
-  void addFeatureLinks(final SequenceI seq, List<String> links)
+  void addLinks(final SequenceI seq, List<SequenceFeature> features)
   {
     JMenu linkMenu = new JMenu(MessageManager.getString("action.link"));
+
+    List<String> nlinks = null;
+    if (seq != null)
+    {
+      nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
+    }
+    else
+    {
+      nlinks = new ArrayList<>();
+    }
+
+    if (features != null)
+    {
+      for (SequenceFeature sf : features)
+      {
+        if (sf.links != null)
+        {
+          for (String link : sf.links)
+          {
+            nlinks.add(link);
+          }
+        }
+      }
+    }
+
     Map<String, List<String>> linkset = new LinkedHashMap<>();
 
-    for (String link : links)
+    for (String link : nlinks)
     {
       UrlLink urlLink = null;
       try
@@ -548,25 +642,18 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
     addshowLinks(linkMenu, linkset.values());
 
-    // disable link menu if there are no valid entries
+    // only add link menu if it has entries
     if (linkMenu.getItemCount() > 0)
     {
-      linkMenu.setEnabled(true);
-    }
-    else
-    {
-      linkMenu.setEnabled(false);
-    }
-
-    if (sequence != null)
-    {
-      sequenceMenu.add(linkMenu);
-    }
-    else
-    {
-      add(linkMenu);
+      if (sequence != null)
+      {
+        sequenceMenu.add(linkMenu);
+      }
+      else
+      {
+        add(linkMenu);
+      }
     }
-
   }
 
   /**
index e99e577..ad80a3e 100644 (file)
@@ -59,7 +59,6 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -1782,21 +1781,10 @@ public class SeqPanel extends JPanel
     final int column = findColumn(evt);
     final int seq = findSeq(evt);
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
+    List<SequenceFeature> features = ap.getFeatureRenderer()
             .findFeaturesAtColumn(sequence, column + 1);
-    List<String> links = new ArrayList<>();
-    for (SequenceFeature sf : allFeatures)
-    {
-      if (sf.links != null)
-      {
-        for (String link : sf.links)
-        {
-          links.add(link);
-        }
-      }
-    }
 
-    PopupMenu pop = new PopupMenu(ap, null, links);
+    PopupMenu pop = new PopupMenu(ap, null, features);
     pop.show(this, evt.getX(), evt.getY());
   }
 
index c7e1d7a..a25a014 100644 (file)
@@ -39,6 +39,8 @@ import java.util.Map;
  */
 public class Gff3Helper extends GffHelperBase
 {
+  public static final String ALLELES = "alleles";
+
   protected static final String TARGET = "Target";
 
   protected static final String ID = "ID";
@@ -399,7 +401,7 @@ public class Gff3Helper extends GffHelperBase
       /*
        * Ensembl returns dna variants as 'alleles'
        */
-      desc = StringUtils.listToDelimitedString(attributes.get("alleles"),
+      desc = StringUtils.listToDelimitedString(attributes.get(ALLELES),
               ",");
     }
 
diff --git a/src/jalview/io/vcf/VCFLoader.java b/src/jalview/io/vcf/VCFLoader.java
new file mode 100644 (file)
index 0000000..78d2ad5
--- /dev/null
@@ -0,0 +1,569 @@
+package jalview.io.vcf;
+
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFHeader;
+import htsjdk.variant.vcf.VCFHeaderLine;
+
+import jalview.analysis.AlignmentUtils;
+import jalview.analysis.Dna;
+import jalview.api.AlignViewControllerGuiI;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.ext.ensembl.EnsemblMap;
+import jalview.ext.htsjdk.VCFReader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A class to read VCF data (using the htsjdk) and add variants as sequence
+ * features on dna and any related protein product sequences
+ * 
+ * @author gmcarstairs
+ */
+public class VCFLoader
+{
+  private static final String FEATURE_GROUP_VCF = "VCF";
+
+  private static final String EXCL = "!";
+
+  /*
+   * the alignment we are associated VCF data with
+   */
+  private AlignmentI al;
+
+  /*
+   * mappings between VCF and sequence reference assembly regions, as 
+   * key = "species!chromosome!fromAssembly!toAssembly
+   * value = Map{fromRange, toRange}
+   */
+  private Map<String, Map<int[], int[]>> assemblyMappings;
+
+  /**
+   * Constructor given an alignment context
+   * 
+   * @param alignment
+   */
+  public VCFLoader(AlignmentI alignment)
+  {
+    al = alignment;
+
+    // map of species!chromosome!fromAssembly!toAssembly to {fromRange, toRange}
+    assemblyMappings = new HashMap<String, Map<int[], int[]>>();
+  }
+
+  /**
+   * Loads VCF on to an alignment - provided it can be related to one or more
+   * sequence's chromosomal coordinates.
+   * <p>
+   * This method is not thread safe - concurrent threads should use separate
+   * instances of this class.
+   * 
+   * @param filePath
+   * @param status
+   */
+  public void loadVCF(String filePath, AlignViewControllerGuiI status)
+  {
+    VCFReader reader = null;
+
+    try
+    {
+      // long start = System.currentTimeMillis();
+      reader = new VCFReader(filePath);
+
+      VCFHeader header = reader.getFileHeader();
+      VCFHeaderLine ref = header
+              .getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
+      // check if reference is wrt assembly19 (GRCh37)
+      // todo may need to allow user to specify reference assembly?
+      boolean isRefGrch37 = (ref != null && ref.getValue().contains(
+              "assembly19"));
+
+      int varCount = 0;
+      int seqCount = 0;
+
+      /*
+       * query for VCF overlapping each sequence in turn
+       */
+      for (SequenceI seq : al.getSequences())
+      {
+        int added = loadVCF(seq, reader, isRefGrch37);
+        if (added > 0)
+        {
+          seqCount++;
+          varCount += added;
+          transferAddedFeatures(seq);
+        }
+      }
+      // long elapsed = System.currentTimeMillis() - start;
+      String msg = String.format("Added %d VCF variants to %d sequence(s)",
+              varCount, seqCount);
+      if (status != null)
+      {
+        status.setStatus(msg);
+      }
+    } catch (Throwable e)
+    {
+      System.err.println("Error processing VCF: " + e.getMessage());
+      e.printStackTrace();
+    } finally
+    {
+      if (reader != null)
+      {
+        try
+        {
+          reader.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+    }
+  }
+
+  /**
+   * Transfers VCF features to sequences to which this sequence has a mapping.
+   * If the mapping is 1:3, computes peptide variants from nucleotide variants.
+   * 
+   * @param seq
+   */
+  protected void transferAddedFeatures(SequenceI seq)
+  {
+    DBRefEntry[] dbrefs = seq.getDBRefs();
+    if (dbrefs == null)
+    {
+      return;
+    }
+    for (DBRefEntry dbref : dbrefs)
+    {
+      Mapping mapping = dbref.getMap();
+      if (mapping == null || mapping.getTo() == null)
+      {
+        continue;
+      }
+
+      SequenceI mapTo = mapping.getTo();
+      MapList map = mapping.getMap();
+      if (map.getFromRatio() == 3)
+      {
+        /*
+         * dna-to-peptide product mapping
+         */
+        AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
+      }
+      else
+      {
+        /*
+         * nucleotide-to-nucleotide mapping e.g. transcript to CDS
+         */
+        // TODO no DBRef to CDS is added to transcripts
+        List<SequenceFeature> features = seq.getFeatures()
+                .getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT);
+        for (SequenceFeature sf : features)
+        {
+          if (FEATURE_GROUP_VCF.equals(sf.getFeatureGroup()))
+          {
+            transferFeature(sf, mapTo, map);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Tries to add overlapping variants read from a VCF file to the given
+   * sequence, and returns the number of overlapping variants found. Note that
+   * this requires the sequence to hold information as to its chromosomal
+   * positions and reference, in order to be able to map the VCF variants to the
+   * sequence.
+   * 
+   * @param seq
+   * @param reader
+   * @param isVcfRefGrch37
+   * @return
+   */
+  protected int loadVCF(SequenceI seq, VCFReader reader,
+          boolean isVcfRefGrch37)
+  {
+    int count = 0;
+    GeneLociI seqCoords = seq.getGeneLoci();
+    if (seqCoords == null)
+    {
+      return 0;
+    }
+
+    List<int[]> seqChromosomalContigs = seqCoords.getMap().getToRanges();
+    for (int[] range : seqChromosomalContigs)
+    {
+      count += addVcfVariants(seq, reader, range, isVcfRefGrch37);
+    }
+
+    return count;
+  }
+
+  /**
+   * Queries the VCF reader for any variants that overlap the given chromosome
+   * region of the sequence, and adds as variant features. Returns the number of
+   * overlapping variants found.
+   * 
+   * @param seq
+   * @param reader
+   * @param range
+   *          start-end range of a sequence region in its chromosomal
+   *          coordinates
+   * @param isVcfRefGrch37
+   *          true if the VCF is with reference to GRCh37
+   * @return
+   */
+  protected int addVcfVariants(SequenceI seq, VCFReader reader,
+          int[] range, boolean isVcfRefGrch37)
+  {
+    GeneLociI seqCoords = seq.getGeneLoci();
+
+    String chromosome = seqCoords.getChromosomeId();
+    String seqRef = seqCoords.getAssemblyId();
+    String species = seqCoords.getSpeciesId();
+
+    // TODO handle species properly
+    if ("".equals(species))
+    {
+      species = "human";
+    }
+
+    /*
+     * map chromosomal coordinates from GRCh38 (sequence) to
+     * GRCh37 (VCF) if necessary
+     */
+    // TODO generalise for other assemblies and species
+    int offset = 0;
+    String fromRef = "GRCh38";
+    if (fromRef.equalsIgnoreCase(seqRef) && isVcfRefGrch37)
+    {
+      String toRef = "GRCh37";
+      int[] newRange = mapReferenceRange(range, chromosome, species,
+              fromRef, toRef);
+      if (newRange == null)
+      {
+        System.err.println(String.format(
+                "Failed to map %s:%s:%s:%d:%d to %s", species, chromosome,
+                fromRef, range[0], range[1], toRef));
+        return 0;
+      }
+      offset = newRange[0] - range[0];
+      range = newRange;
+    }
+
+    boolean forwardStrand = range[0] <= range[1];
+
+    /*
+     * query the VCF for overlaps
+     * (convert a reverse strand range to forwards)
+     */
+    int count = 0;
+    MapList mapping = seqCoords.getMap();
+
+    int fromLocus = Math.min(range[0], range[1]);
+    int toLocus = Math.max(range[0], range[1]);
+    CloseableIterator<VariantContext> variants = reader.query(chromosome,
+            fromLocus, toLocus);
+    while (variants.hasNext())
+    {
+      /*
+       * get variant location in sequence chromosomal coordinates
+       */
+      VariantContext variant = variants.next();
+
+      /*
+       * we can only process SNP variants (which can be reported
+       * as part of a MIXED variant record
+       */
+      if (!variant.isSNP() && !variant.isMixed())
+      {
+        continue;
+      }
+
+      count++;
+      int start = variant.getStart() - offset;
+      int end = variant.getEnd() - offset;
+
+      /*
+       * convert chromosomal location to sequence coordinates
+       * - null if a partially overlapping feature
+       */
+      int[] seqLocation = mapping.locateInFrom(start, end);
+      if (seqLocation != null)
+      {
+        addVariantFeatures(seq, variant, seqLocation[0], seqLocation[1],
+                forwardStrand);
+      }
+    }
+
+    variants.close();
+
+    return count;
+  }
+
+  /**
+   * Inspects the VCF variant record, and adds variant features to the sequence.
+   * Only SNP variants are added, not INDELs.
+   * <p>
+   * If the sequence maps to the reverse strand of the chromosome, reference and
+   * variant bases are recorded as their complements (C/G, A/T).
+   * 
+   * @param seq
+   * @param variant
+   * @param featureStart
+   * @param featureEnd
+   * @param forwardStrand
+   */
+  protected void addVariantFeatures(SequenceI seq, VariantContext variant,
+          int featureStart, int featureEnd, boolean forwardStrand)
+  {
+    byte[] reference = variant.getReference().getBases();
+    if (reference.length != 1)
+    {
+      /*
+       * sorry, we don't handle INDEL variants
+       */
+      return;
+    }
+
+    /*
+     * for now we extract allele frequency as feature score; note
+     * this attribute is String for a simple SNP, but List<String> if
+     * multiple alleles at the locus; we extract for the simple case only
+     */
+    Object af = variant.getAttribute("AF");
+    float score = 0f;
+    if (af instanceof String)
+    {
+      try
+      {
+        score = Float.parseFloat((String) af);
+      } catch (NumberFormatException e)
+      {
+        // leave as 0
+      }
+    }
+
+    StringBuilder sb = new StringBuilder();
+    sb.append(forwardStrand ? (char) reference[0] : complement(reference));
+
+    /*
+     * inspect alleles and record SNP variants (as the variant
+     * record could be MIXED and include INDEL and SNP alleles)
+     * warning: getAlleles gives no guarantee as to the order 
+     * in which they are returned
+     */
+    for (Allele allele : variant.getAlleles())
+    {
+      if (!allele.isReference())
+      {
+        byte[] alleleBase = allele.getBases();
+        if (alleleBase.length == 1)
+        {
+          sb.append(",").append(
+                  forwardStrand ? (char) alleleBase[0]
+                          : complement(alleleBase));
+        }
+      }
+    }
+    String alleles = sb.toString(); // e.g. G,A,C
+
+    String type = SequenceOntologyI.SEQUENCE_VARIANT;
+
+    SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
+            featureEnd, score, FEATURE_GROUP_VCF);
+
+    sf.setValue(Gff3Helper.ALLELES, alleles);
+
+    Map<String, Object> atts = variant.getAttributes();
+    for (Entry<String, Object> att : atts.entrySet())
+    {
+      sf.setValue(att.getKey(), att.getValue());
+    }
+    seq.addSequenceFeature(sf);
+  }
+
+  /**
+   * A convenience method to complement a dna base and return the string value
+   * of its complement
+   * 
+   * @param reference
+   * @return
+   */
+  protected String complement(byte[] reference)
+  {
+    return String.valueOf(Dna.getComplement((char) reference[0]));
+  }
+
+  /**
+   * Determines the location of the query range (chromosome positions) in a
+   * different reference assembly.
+   * <p>
+   * If the range is just a subregion of one for which we already have a mapping
+   * (for example, an exon sub-region of a gene), then the mapping is just
+   * computed arithmetically.
+   * <p>
+   * Otherwise, calls the Ensembl REST service that maps from one assembly
+   * reference's coordinates to another's
+   * 
+   * @param queryRange
+   *          start-end chromosomal range in 'fromRef' coordinates
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   *          assembly reference for the query coordinates
+   * @param toRef
+   *          assembly reference we wish to translate to
+   * @return the start-end range in 'toRef' coordinates
+   */
+  protected int[] mapReferenceRange(int[] queryRange, String chromosome,
+          String species, String fromRef, String toRef)
+  {
+    /*
+     * first try shorcut of computing the mapping as a subregion of one
+     * we already have (e.g. for an exon, if we have the gene mapping)
+     */
+    int[] mappedRange = findSubsumedRangeMapping(queryRange, chromosome,
+            species, fromRef, toRef);
+    if (mappedRange != null)
+    {
+      return mappedRange;
+    }
+
+    /*
+     * call (e.g.) http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37
+     */
+    EnsemblMap mapper = new EnsemblMap();
+    int[] mapping = mapper.getMapping(species, chromosome, fromRef, toRef,
+            queryRange);
+
+    if (mapping == null)
+    {
+      // mapping service failure
+      return null;
+    }
+
+    /*
+     * save mapping for possible future re-use
+     */
+    String key = makeRangesKey(chromosome, species, fromRef, toRef);
+    if (!assemblyMappings.containsKey(key))
+    {
+      assemblyMappings.put(key, new HashMap<int[], int[]>());
+    }
+
+    assemblyMappings.get(key).put(queryRange, mapping);
+
+    return mapping;
+  }
+
+  /**
+   * If we already have a 1:1 contiguous mapping which subsumes the given query
+   * range, this method just calculates and returns the subset of that mapping,
+   * else it returns null. In practical terms, if a gene has a contiguous
+   * mapping between (for example) GRCh37 and GRCh38, then we assume that its
+   * subsidiary exons occupy unchanged relative positions, and just compute
+   * these as offsets, rather than do another lookup of the mapping.
+   * <p>
+   * If in future these assumptions prove invalid (e.g. for bacterial dna?!),
+   * simply remove this method or let it always return null.
+   * <p>
+   * Warning: many rapid calls to the /map service map result in a 429 overload
+   * error response
+   * 
+   * @param queryRange
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   * @param toRef
+   * @return
+   */
+  protected int[] findSubsumedRangeMapping(int[] queryRange, String chromosome,
+          String species, String fromRef, String toRef)
+  {
+    String key = makeRangesKey(chromosome, species, fromRef, toRef);
+    if (assemblyMappings.containsKey(key))
+    {
+      Map<int[], int[]> mappedRanges = assemblyMappings.get(key);
+      for (Entry<int[], int[]> mappedRange : mappedRanges.entrySet())
+      {
+        int[] fromRange = mappedRange.getKey();
+        int[] toRange = mappedRange.getValue();
+        if (fromRange[1] - fromRange[0] == toRange[1] - toRange[0])
+        {
+          /*
+           * mapping is 1:1 in length, so we trust it to have no discontinuities
+           */
+          if (MappingUtils.rangeContains(fromRange, queryRange))
+          {
+            /*
+             * fromRange subsumes our query range
+             */
+            int offset = queryRange[0] - fromRange[0];
+            int mappedRangeFrom = toRange[0] + offset;
+            int mappedRangeTo = mappedRangeFrom + (queryRange[1] - queryRange[0]);
+            return new int[] { mappedRangeFrom, mappedRangeTo };
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Transfers the sequence feature to the target sequence, locating its start
+   * and end range based on the mapping. Features which do not overlap the
+   * target sequence are ignored.
+   * 
+   * @param sf
+   * @param targetSequence
+   * @param mapping
+   *          mapping from the feature's coordinates to the target sequence
+   */
+  protected void transferFeature(SequenceFeature sf,
+          SequenceI targetSequence, MapList mapping)
+  {
+    int[] mappedRange = mapping.locateInTo(sf.getBegin(), sf.getEnd());
+  
+    if (mappedRange != null)
+    {
+      String group = sf.getFeatureGroup();
+      int newBegin = Math.min(mappedRange[0], mappedRange[1]);
+      int newEnd = Math.max(mappedRange[0], mappedRange[1]);
+      SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+              group, sf.getScore());
+      targetSequence.addSequenceFeature(copy);
+    }
+  }
+
+  /**
+   * Formats a ranges map lookup key
+   * 
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   * @param toRef
+   * @return
+   */
+  protected static String makeRangesKey(String chromosome, String species,
+          String fromRef, String toRef)
+  {
+    return species + EXCL + chromosome + EXCL + fromRef + EXCL
+            + toRef;
+  }
+}
index 86d0c85..1cf482d 100755 (executable)
@@ -147,6 +147,8 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenuItem runGroovy = new JMenuItem();
 
+  protected JMenuItem loadVcf;
+
   protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem();
 
   protected JCheckBoxMenuItem sortByTree = new JCheckBoxMenuItem();
@@ -1308,6 +1310,16 @@ public class GAlignFrame extends JInternalFrame
         associatedData_actionPerformed(e);
       }
     });
+    loadVcf = new JMenuItem(MessageManager.getString("label.load_vcf_file"));
+    loadVcf.setToolTipText(MessageManager.getString("label.load_vcf"));
+    loadVcf.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        loadVcf_actionPerformed();
+      }
+    });
     autoCalculate.setText(
             MessageManager.getString("label.autocalculate_consensus"));
     autoCalculate.setState(
@@ -1710,6 +1722,7 @@ public class GAlignFrame extends JInternalFrame
     fileMenu.add(exportAnnotations);
     fileMenu.add(loadTreeMenuItem);
     fileMenu.add(associatedData);
+    fileMenu.add(loadVcf);
     fileMenu.addSeparator();
     fileMenu.add(closeMenuItem);
 
@@ -1855,6 +1868,10 @@ public class GAlignFrame extends JInternalFrame
     // selectMenu.add(listenToViewSelections);
   }
 
+  protected void loadVcf_actionPerformed()
+  {
+  }
+
   /**
    * Constructs the entries on the Colour menu (but does not add them to the
    * menu).
index 3682239..d21eac3 100644 (file)
@@ -939,4 +939,32 @@ public final class MappingUtils
     }
     return copy;
   }
+
+  /**
+   * Answers true if range's start-end positions include those of queryRange,
+   * where either range might be in reverse direction, else false
+   * 
+   * @param range
+   *          a start-end range
+   * @param queryRange
+   *          a candidate subrange of range (start2-end2)
+   * @return
+   */
+  public static boolean rangeContains(int[] range, int[] queryRange)
+  {
+    if (range == null || queryRange == null || range.length != 2
+            || queryRange.length != 2)
+    {
+      /*
+       * invalid arguments
+       */
+      return false;
+    }
+
+    int min = Math.min(range[0], range[1]);
+    int max = Math.max(range[0], range[1]);
+  
+    return (min <= queryRange[0] && max >= queryRange[0]
+            && min <= queryRange[1] && max >= queryRange[1]);
+  }
 }
index 4439bb9..7c64193 100644 (file)
@@ -1044,14 +1044,18 @@ public class AlignmentUtilsTests
     dna.addCodonFrame(acf);
 
     /*
-     * In this case, mappings originally came from matching Uniprot accessions - so need an xref on dna involving those regions. These are normally constructed from CDS annotation
+     * In this case, mappings originally came from matching Uniprot accessions 
+     * - so need an xref on dna involving those regions. 
+     * These are normally constructed from CDS annotation
      */
     DBRefEntry dna1xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep1",
             new Mapping(mapfordna1));
-    dna1.getDatasetSequence().addDBRef(dna1xref);
+    dna1.addDBRef(dna1xref);
+    assertEquals(2, dna1.getDBRefs().length); // to self and to pep1
     DBRefEntry dna2xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep2",
             new Mapping(mapfordna2));
-    dna2.getDatasetSequence().addDBRef(dna2xref);
+    dna2.addDBRef(dna2xref);
+    assertEquals(2, dna2.getDBRefs().length); // to self and to pep2
 
     /*
      * execute method under test:
@@ -1106,6 +1110,38 @@ public class AlignmentUtilsTests
     assertEquals(cdsMapping.getInverse(), dbref.getMap().getMap());
 
     /*
+     * verify cDNA has added a dbref with mapping to CDS
+     */
+    assertEquals(3, dna1.getDBRefs().length);
+    DBRefEntry dbRefEntry = dna1.getDBRefs()[2];
+    assertSame(cds1Dss, dbRefEntry.getMap().getTo());
+    MapList dnaToCdsMapping = new MapList(new int[] { 4, 6, 10, 12 },
+            new int[] { 1, 6 }, 1, 1);
+    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
+    assertEquals(3, dna2.getDBRefs().length);
+    dbRefEntry = dna2.getDBRefs()[2];
+    assertSame(cds2Dss, dbRefEntry.getMap().getTo());
+    dnaToCdsMapping = new MapList(new int[] { 1, 3, 7, 9, 13, 15 },
+            new int[] { 1, 9 }, 1, 1);
+    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
+
+    /*
+     * verify CDS has added a dbref with mapping to cDNA
+     */
+    assertEquals(2, cds1Dss.getDBRefs().length);
+    dbRefEntry = cds1Dss.getDBRefs()[1];
+    assertSame(dna1.getDatasetSequence(), dbRefEntry.getMap().getTo());
+    MapList cdsToDnaMapping = new MapList(new int[] { 1, 6 }, new int[] {
+        4, 6, 10, 12 }, 1, 1);
+    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
+    assertEquals(2, cds2Dss.getDBRefs().length);
+    dbRefEntry = cds2Dss.getDBRefs()[1];
+    assertSame(dna2.getDatasetSequence(), dbRefEntry.getMap().getTo());
+    cdsToDnaMapping = new MapList(new int[] { 1, 9 }, new int[] { 1, 3, 7,
+        9, 13, 15 }, 1, 1);
+    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
+
+    /*
      * Verify mappings from CDS to peptide, cDNA to CDS, and cDNA to peptide
      * the mappings are on the shared alignment dataset
      * 6 mappings, 2*(DNA->CDS), 2*(DNA->Pep), 2*(CDS->Pep) 
diff --git a/test/jalview/ext/htsjdk/VCFReaderTest.java b/test/jalview/ext/htsjdk/VCFReaderTest.java
new file mode 100644 (file)
index 0000000..bf617ae
--- /dev/null
@@ -0,0 +1,200 @@
+package jalview.ext.htsjdk;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class VCFReaderTest
+{
+  private static final String[] VCF = new String[] {
+      "##fileformat=VCFv4.2",
+      "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+      "20\t3\t.\tC\tG\t.\tPASS\tDP=100", // SNP C/G
+      "20\t7\t.\tG\tGA\t.\tPASS\tDP=100", // insertion G/GA
+      "18\t2\t.\tACG\tA\t.\tPASS\tDP=100" }; // deletion ACG/A
+
+  // gnomAD exome variant dataset
+  private static final String VCF_PATH = "/Volumes/gjb/smacgowan/NOBACK/resources/gnomad/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+  // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+  /**
+   * A test to exercise some basic functionality of the htsjdk VCF reader,
+   * reading from a non-index VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testReadVcf_plain() throws IOException
+  {
+    File f = writeVcfFile();
+    VCFReader reader = new VCFReader(f.getAbsolutePath());
+    CloseableIterator<VariantContext> variants = reader.iterator();
+
+    /*
+     * SNP C/G variant
+     */
+    VariantContext vc = variants.next();
+    assertTrue(vc.isSNP());
+    Allele ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "C");
+    List<Allele> alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "C");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "G");
+
+    /*
+     * Insertion G -> GA
+     */
+    vc = variants.next();
+    assertFalse(vc.isSNP());
+    assertTrue(vc.isSimpleInsertion());
+    ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "G");
+    alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "G");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "GA");
+
+    /*
+     * Deletion ACG -> A
+     */
+    vc = variants.next();
+    assertFalse(vc.isSNP());
+    assertTrue(vc.isSimpleDeletion());
+    ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "ACG");
+    alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "ACG");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "A");
+
+    assertFalse(variants.hasNext());
+
+    variants.close();
+    reader.close();
+  }
+
+  /**
+   * Creates a temporary file to be read by the htsjdk VCF reader
+   * 
+   * @return
+   * @throws IOException
+   */
+  protected File writeVcfFile() throws IOException
+  {
+    File f = File.createTempFile("Test", "vcf");
+    f.deleteOnExit();
+    PrintWriter pw = new PrintWriter(f);
+    for (String vcfLine : VCF) {
+      pw.println(vcfLine);
+    }
+    pw.close();
+    return f;
+  }
+  
+  /**
+   * A 'test' that demonstrates querying an indexed VCF file for features in a
+   * specified interval
+   * 
+   * @throws IOException
+   */
+  @Test
+  public void testQuery_indexed() throws IOException
+  {
+    /*
+     * if not specified, assumes index file is filename.tbi
+     */
+    VCFReader reader = new VCFReader(VCF_PATH);
+  
+    /*
+     * gene NMT1 (human) is on chromosome 17
+     * GCHR38 (Ensembl): 45051610-45109016
+     * GCHR37 (gnoMAD): 43128978-43186384
+     * CDS begins at offset 9720, first CDS variant at offset 9724
+     */
+    CloseableIterator<VariantContext> features = reader.query("17",
+            43128978 + 9724, 43128978 + 9734); // first 11 CDS positions
+
+    assertEquals(printNext(features), 43138702);
+    assertEquals(printNext(features), 43138704);
+    assertEquals(printNext(features), 43138707);
+    assertEquals(printNext(features), 43138708);
+    assertEquals(printNext(features), 43138710);
+    assertEquals(printNext(features), 43138711);
+    assertFalse(features.hasNext());
+
+    features.close();
+    reader.close();
+  }
+
+  /**
+   * Prints the toString value of the next variant, and returns its start
+   * location
+   * 
+   * @param features
+   * @return
+   */
+  protected int printNext(CloseableIterator<VariantContext> features)
+  {
+    VariantContext next = features.next();
+    System.out.println(next.toString());
+    return next.getStart();
+  }
+
+  // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+  
+  /**
+   * Test the query method that wraps a non-indexed VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testQuery_plain() throws IOException
+  {
+    File f = writeVcfFile();
+    VCFReader reader = new VCFReader(f.getAbsolutePath());
+
+    /*
+     * query for overlap of 5-8 - should find variant at 7
+     */
+    CloseableIterator<VariantContext> variants = reader.query("20", 5, 8);
+  
+    /*
+     * INDEL G/GA variant
+     */
+    VariantContext vc = variants.next();
+    assertTrue(vc.isIndel());
+    assertEquals(vc.getStart(), 7);
+    assertEquals(vc.getEnd(), 7);
+    Allele ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "G");
+    List<Allele> alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "G");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "GA");
+
+    assertFalse(variants.hasNext());
+
+    variants.close();
+    reader.close();
+  }
+}
index 335240b..40e624d 100644 (file)
@@ -26,21 +26,26 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
-import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
 import jalview.io.FormatAdapter;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
 import jalview.util.MessageManager;
+import jalview.util.UrlConstants;
 
 import java.awt.Component;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import javax.swing.JMenu;
@@ -80,6 +85,25 @@ public class PopupMenuTest
   @BeforeMethod(alwaysRun = true)
   public void setUp() throws IOException
   {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    String inMenuString = ("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$"
+            + SEQUENCE_ID
+            + "$"
+            + "|"
+            + "UNIPROT | http://www.uniprot.org/uniprot/$" + DB_ACCESSION + "$")
+            + "|"
+            + ("INTERPRO | http://www.ebi.ac.uk/interpro/entry/$"
+                    + DB_ACCESSION + "$")
+            + "|"
+            +
+            // Gene3D entry tests for case (in)sensitivity
+            ("Gene3D | http://gene3d.biochem.ucl.ac.uk/Gene3D/search?sterm=$"
+                    + DB_ACCESSION + "$&mode=protein");
+
+    UrlProviderFactoryI factory = new DesktopUrlProviderFactory(
+            UrlConstants.DEFAULT_LABEL, inMenuString, "");
+    Preferences.sequenceUrlLinks = factory.createUrlProvider();
+
     alignment = new FormatAdapter().readFile(TEST_DATA,
             DataSourceType.PASTE, FileFormat.Fasta);
     AlignFrame af = new AlignFrame(alignment, 700, 500);
@@ -495,17 +519,19 @@ public class PopupMenuTest
 
     // add all the dbrefs to the sequences: Uniprot 1 each, Interpro all 3 to
     // seq0, Gene3D to seq1
-    seqs.get(0).addDBRef(refs.get(0));
+    SequenceI seq = seqs.get(0);
+    seq.addDBRef(refs.get(0));
 
-    seqs.get(0).addDBRef(refs.get(1));
-    seqs.get(0).addDBRef(refs.get(2));
-    seqs.get(0).addDBRef(refs.get(3));
+    seq.addDBRef(refs.get(1));
+    seq.addDBRef(refs.get(2));
+    seq.addDBRef(refs.get(3));
     
     seqs.get(1).addDBRef(refs.get(4));
     seqs.get(1).addDBRef(refs.get(5));
     
     // get the Popup Menu for first sequence
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0), links);
+    List<SequenceFeature> noFeatures = Collections.<SequenceFeature> emptyList();
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     Component[] seqItems = testee.sequenceMenu.getMenuComponents();
     JMenu linkMenu = (JMenu) seqItems[6];
     Component[] linkItems = linkMenu.getMenuComponents();
@@ -519,15 +545,18 @@ public class PopupMenuTest
     // sequence id for each link should match corresponding DB accession id
     for (int i = 1; i < 4; i++)
     {
-      assertEquals(refs.get(i - 1).getSource(), ((JMenuItem) linkItems[i])
+      String msg = seq.getName() + " link[" + i + "]";
+      assertEquals(msg, refs.get(i - 1).getSource(),
+              ((JMenuItem) linkItems[i])
               .getText().split("\\|")[0]);
-      assertEquals(refs.get(i - 1).getAccessionId(),
+      assertEquals(msg, refs.get(i - 1).getAccessionId(),
               ((JMenuItem) linkItems[i])
               .getText().split("\\|")[1]);
     }
 
     // get the Popup Menu for second sequence
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(1), links);
+    seq = seqs.get(1);
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     seqItems = testee.sequenceMenu.getMenuComponents();
     linkMenu = (JMenu) seqItems[6];
     linkItems = linkMenu.getMenuComponents();
@@ -541,9 +570,11 @@ public class PopupMenuTest
     // sequence id for each link should match corresponding DB accession id
     for (int i = 1; i < 3; i++)
     {
-      assertEquals(refs.get(i + 3).getSource(), ((JMenuItem) linkItems[i])
+      String msg = seq.getName() + " link[" + i + "]";
+      assertEquals(msg, refs.get(i + 3).getSource(),
+              ((JMenuItem) linkItems[i])
               .getText().split("\\|")[0].toUpperCase());
-      assertEquals(refs.get(i + 3).getAccessionId(),
+      assertEquals(msg, refs.get(i + 3).getAccessionId(),
               ((JMenuItem) linkItems[i]).getText().split("\\|")[1]);
     }
 
@@ -552,8 +583,7 @@ public class PopupMenuTest
     nomatchlinks.add("NOMATCH | http://www.uniprot.org/uniprot/$"
             + DB_ACCESSION + "$");
 
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0),
-            nomatchlinks);
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     seqItems = testee.sequenceMenu.getMenuComponents();
     linkMenu = (JMenu) seqItems[6];
     assertFalse(linkMenu.isEnabled());
diff --git a/test/jalview/io/vcf/VCFLoaderTest.java b/test/jalview/io/vcf/VCFLoaderTest.java
new file mode 100644 (file)
index 0000000..7379ecd
--- /dev/null
@@ -0,0 +1,287 @@
+package jalview.io.vcf;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class VCFLoaderTest
+{
+  // columns 9717- of gene P30419 from Ensembl (modified)
+  private static final String FASTA =
+  // forward strand 'gene'
+  ">gene1/1-25 chromosome:GRCh38:17:45051610:45051634:1\n"
+          + "CAAGCTGGCGGACGAGAGTGTGACA\n"
+          // and a 'made up' mini-transcript with two exons
+          + ">transcript1/1-18\n--AGCTGGCG----AGAGTGTGAC-\n"
+          +
+          // 'reverse strand' gene (reverse complement)
+          ">gene2/1-25 chromosome:GRCh38:17:45051610:45051634:-1\n"
+          + "TGTCACACTCTCGTCCGCCAGCTTG\n"
+          // and its 'transcript'
+          + ">transcript2/1-18\n"
+          + "-GTCACACTCT----CGCCAGCT--\n";
+
+  private static final String[] VCF = { "##fileformat=VCFv4.2",
+      "##INFO=<ID=AF,Number=A,Type=Float,Description=\"Allele Frequency, for each ALT allele, in the same order as listed\">",
+      "##reference=GRCh38",
+      "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+      // SNP A/T in position 2 of gene sequence (precedes transcript)
+      "17\t45051611\t.\tA\tT\t1666.64\tRF\tAC=15;AF=5.08130e-03",
+      // SNP G/C in position 4 of gene sequence, position 2 of transcript
+      // this is a mixed variant, the insertion G/GA is not transferred
+      "17\t45051613\t.\tG\tGA,C\t1666.64\tRF\tAC=15;AF=3.08130e-03" };
+
+  @Test(groups = "Functional")
+  public void testLoadVCF() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+    VCFLoader loader = new VCFLoader(al);
+
+    File f = makeVcf();
+
+    loader.loadVCF(f.getPath(), null);
+
+    /*
+     * verify variant feature(s) added to gene
+     */
+    List<SequenceFeature> geneFeatures = al.getSequenceAt(0)
+            .getSequenceFeatures();
+    assertEquals(geneFeatures.size(), 2);
+    SequenceFeature sf = geneFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 5.08130e-03, 0.000001f);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,T");
+
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 4);
+    assertEquals(sf.getEnd(), 4);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.08130e-03, 0.000001f);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+
+    /*
+     * verify variant feature(s) added to transcript
+     */
+    List<SequenceFeature> transcriptFeatures = al.getSequenceAt(1)
+            .getSequenceFeatures();
+    assertEquals(transcriptFeatures.size(), 1);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.08130e-03, 0.000001f);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+
+    /*
+     * verify variant feature(s) computed and added to protein
+     * first codon AGC varies to ACC giving S/T
+     */
+    DBRefEntry[] dbRefs = al.getSequenceAt(1).getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    assertEquals(proteinFeatures.size(), 1);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 1);
+    assertEquals(sf.getEnd(), 1);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getDescription(), "p.Ser1Thr");
+  }
+
+  private File makeVcf() throws IOException
+  {
+    File f = File.createTempFile("Test", ".vcf");
+    f.deleteOnExit();
+    PrintWriter pw = new PrintWriter(f);
+    for (String vcfLine : VCF)
+    {
+      pw.println(vcfLine);
+    }
+    pw.close();
+    return f;
+  }
+
+  /**
+   * Make a simple alignment with one 'gene' and one 'transcript'
+   * 
+   * @return
+   */
+  private AlignmentI buildAlignment()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(FASTA,
+            DataSourceType.PASTE);
+
+    /*
+     * map gene1 sequence to chromosome (normally done when the sequence is fetched
+     * from Ensembl and transcripts computed)
+     */
+    AlignmentI alignment = af.getViewport().getAlignment();
+    SequenceI gene1 = alignment.getSequenceAt(0);
+    int[] to = new int[] { 45051610, 45051634 };
+    int[] from = new int[] { gene1.getStart(), gene1.getEnd() };
+    gene1.setGeneLoci("human", "GRCh38", "17", new MapList(from, to, 1, 1));
+
+    /*
+     * map 'transcript1' to chromosome via 'gene1'
+     * transcript1/1-18 is gene1/3-10,15-24
+     * which is chromosome 45051612-45051619,45051624-45051633
+     */
+    to = new int[] { 45051612, 45051619, 45051624, 45051633 };
+    SequenceI transcript1 = alignment.getSequenceAt(1);
+    from = new int[] { transcript1.getStart(), transcript1.getEnd() };
+    transcript1.setGeneLoci("human", "GRCh38", "17", new MapList(from, to,
+            1, 1));
+
+    /*
+     * map gene2 to chromosome reverse strand
+     */
+    SequenceI gene2 = alignment.getSequenceAt(2);
+    to = new int[] { 45051634, 45051610 };
+    from = new int[] { gene2.getStart(), gene2.getEnd() };
+    gene2.setGeneLoci("human", "GRCh38", "17", new MapList(from, to, 1, 1));
+
+    /*
+     * map 'transcript2' to chromosome via 'gene2'
+     * transcript2/1-18 is gene2/2-11,16-23
+     * which is chromosome 45051633-45051624,45051619-45051612
+     */
+    to = new int[] { 45051633, 45051624, 45051619, 45051612 };
+    SequenceI transcript2 = alignment.getSequenceAt(3);
+    from = new int[] { transcript2.getStart(), transcript2.getEnd() };
+    transcript2.setGeneLoci("human", "GRCh38", "17", new MapList(from, to,
+            1, 1));
+
+    /*
+     * add a protein product as a DBRef on transcript1
+     */
+    SequenceI peptide1 = new Sequence("ENSP001", "SWRECD");
+    MapList mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 },
+            3, 1);
+    Mapping map = new Mapping(peptide1, mapList);
+    DBRefEntry product = new DBRefEntry("", "", "ENSP001", map);
+    transcript1.addDBRef(product);
+
+    /*
+     * add a protein product as a DBRef on transcript2
+     */
+    SequenceI peptide2 = new Sequence("ENSP002", "VTLSPA");
+    mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1);
+    map = new Mapping(peptide2, mapList);
+    product = new DBRefEntry("", "", "ENSP002", map);
+    transcript2.addDBRef(product);
+
+    return alignment;
+  }
+
+  /**
+   * Test with 'gene' and 'transcript' mapped to the reverse strand of the
+   * chromosome. The VCF variant positions (in forward coordinates) should get
+   * correctly located on sequence positions.
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testLoadVCF_reverseStrand() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+
+    VCFLoader loader = new VCFLoader(al);
+
+    File f = makeVcf();
+
+    loader.loadVCF(f.getPath(), null);
+
+    /*
+     * verify variant feature(s) added to gene2
+     * gene/1-25 maps to chromosome 45051634- reverse strand
+     * variants A/T at 45051611 and G/C at 45051613 map to
+     * T/A and C/G at gene positions 24 and 22 respectively
+     */
+    List<SequenceFeature> geneFeatures = al.getSequenceAt(2)
+            .getSequenceFeatures();
+    assertEquals(geneFeatures.size(), 2);
+    SequenceFeature sf = geneFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 22);
+    assertEquals(sf.getEnd(), 22);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.08130e-03, 0.000001f);
+    assertEquals("C,G", sf.getValue(Gff3Helper.ALLELES));
+
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 24);
+    assertEquals(sf.getEnd(), 24);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 5.08130e-03, 0.000001f);
+    assertEquals("T,A", sf.getValue(Gff3Helper.ALLELES));
+
+    /*
+     * verify variant feature(s) added to transcript2
+     * variant C/G at position 22 of gene overlaps and maps to
+     * position 17 of transcript
+     */
+    List<SequenceFeature> transcriptFeatures = al.getSequenceAt(3)
+            .getSequenceFeatures();
+    assertEquals(transcriptFeatures.size(), 1);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 17);
+    assertEquals(sf.getEnd(), 17);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.08130e-03, 0.000001f);
+    assertEquals("C,G", sf.getValue(Gff3Helper.ALLELES));
+
+    /*
+     * verify variant feature(s) computed and added to protein
+     * last codon GCT varies to GGT giving A/G in the last peptide position
+     */
+    DBRefEntry[] dbRefs = al.getSequenceAt(3).getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    assertEquals(proteinFeatures.size(), 1);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 6);
+    assertEquals(sf.getEnd(), 6);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getDescription(), "p.Ala6Gly");
+  }
+}
index d0ec3e8..87070d7 100644 (file)
@@ -1149,4 +1149,93 @@ public class MappingUtilsTest
     assertEquals("[12, 11, 8, 4]", Arrays.toString(ranges));
   }
 
+  @Test(groups = { "Functional" })
+  public void testRangeContains()
+  {
+    /*
+     * both forward ranges
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        2, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 9 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        4, 5 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        0, 9 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        -10, -9 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        11, 12 }));
+
+    /*
+     * forward range, reverse query
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        9, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 2 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        5, 5 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        11, 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 0 }));
+
+    /*
+     * reverse range, forward query
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        1, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        1, 9 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        2, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        6, 6 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        6, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        11, 20 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        -3, -2 }));
+
+    /*
+     * both reverse
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        9, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 2 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        3, 3 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        11, 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 0 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        12, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        -5, -8 }));
+
+    /*
+     * bad arguments
+     */
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10, 12 },
+            new int[] {
+        1, 10 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 },
+            new int[] { 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, null));
+    assertFalse(MappingUtils.rangeContains(null, new int[] { 1, 10 }));
+  }
+
 }