Merge branch 'develop' into feature/JAL-3187linkedFeatures
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 16 Jul 2019 16:13:19 +0000 (17:13 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 16 Jul 2019 16:13:19 +0000 (17:13 +0100)
52 files changed:
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/scoremodels/FeatureDistanceModel.java
src/jalview/api/FeatureRenderer.java
src/jalview/api/ViewStyleI.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/SeqPanel.java
src/jalview/datamodel/AlignedCodonFrame.java
src/jalview/datamodel/MappedFeatures.java [new file with mode: 0644]
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/datamodel/features/SequenceFeaturesI.java
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/CrossRefAction.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/gui/IdPanel.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/OverviewCanvas.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SeqPanel.java
src/jalview/io/FeaturesFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/vcf/VCFLoader.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/seqfeatures/FeatureColourFinder.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/structure/SequenceListener.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/util/MapList.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/styles/ViewStyle.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/datamodel/MappedFeaturesTest.java [new file with mode: 0644]
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/datamodel/features/SequenceFeaturesTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/FeatureSettingsTest.java
test/jalview/gui/PopupMenuTest.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/vcf/VCFLoaderTest.java
test/jalview/io/vcf/testVcf.dat [deleted file]
test/jalview/io/vcf/testVcf.vcf
test/jalview/project/Jalview2xmlTests.java
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java

index d2d8e88..fdd15cd 100644 (file)
@@ -1400,3 +1400,7 @@ label.pca = PCA
 label.create_image_of = Create {0} image of {1}
 label.click_to_edit = Click to edit, right-click for menu
 label.by_annotation_tooltip = Annotation Colour is configured from the main Colour menu
+label.show_linked_features = Show {0} features
+label.on_top = on top
+label.include_linked_features = Include {0} features
+label.include_linked_tooltip = Include visible {0} features<br>converted to local sequence coordinates
\ No newline at end of file
index e9e18ce..daa4418 100644 (file)
@@ -1401,3 +1401,7 @@ label.pca = ACP
 label.create_image_of = Crear imagen {0} de {1}
 label.click_to_edit = Haga clic para editar, clic en el botón derecho para ver el menú  
 label.by_annotation_tooltip = El color de anotación se configura desde el menú principal de colores
+label.show_linked_features = Características de {0}
+label.on_top = encima
+label.include_linked_features = Incluir características de {0}
+label.include_linked_tooltip = Incluir características de {0}<br>convertidas a coordenadas de secuencia local
\ No newline at end of file
index 0bc8180..8e0335f 100644 (file)
@@ -22,6 +22,7 @@ package jalview.analysis;
 
 import static jalview.io.gff.GffConstants.CLINICAL_SIGNIFICANCE;
 
+import jalview.commands.RemoveGapColCommand;
 import jalview.datamodel.AlignedCodon;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
@@ -1746,8 +1747,10 @@ 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[]
+                  { cdsSeq.getStart(),
+                      cdsSeq.getLength() + cdsSeq.getStart() - 1 });
           MapList cdsToProteinMap = new MapList(cdsRange,
                   mapList.getToRanges(), mapList.getFromRatio(),
                   mapList.getToRatio());
@@ -1984,39 +1987,61 @@ public class AlignmentUtils
   static SequenceI makeCdsSequence(SequenceI seq, Mapping mapping,
           AlignmentI dataset)
   {
-    char[] seqChars = seq.getSequence();
-    List<int[]> fromRanges = mapping.getMap().getFromRanges();
-    int cdsWidth = MappingUtils.getLength(fromRanges);
-    char[] newSeqChars = new char[cdsWidth];
+    /*
+     * construct CDS sequence name as "CDS|" with 'from id' held in the mapping
+     * if set (e.g. EMBL protein_id), else sequence name appended
+     */
+    String mapFromId = mapping.getMappedFromId();
+    final String seqId = "CDS|"
+            + (mapFromId != null ? mapFromId : seq.getName());
+
+    SequenceI newSeq = null;
 
-    int newPos = 0;
-    for (int[] range : fromRanges)
+    final MapList maplist = mapping.getMap();
+    if (maplist.isContiguous() && maplist.isFromForwardStrand())
     {
-      if (range[0] <= range[1])
-      {
-        // forward strand mapping - just copy the range
-        int length = range[1] - range[0] + 1;
-        System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos,
-                length);
-        newPos += length;
-      }
-      else
+      /*
+       * just a subsequence, keep same dataset sequence
+       */
+      int start = maplist.getFromLowest();
+      int end = maplist.getFromHighest();
+      newSeq = seq.getSubSequence(start - 1, end);
+      newSeq.setName(seqId);
+    }
+    else
+    {
+      /*
+       * construct by splicing mapped from ranges
+       */
+      char[] seqChars = seq.getSequence();
+      List<int[]> fromRanges = maplist.getFromRanges();
+      int cdsWidth = MappingUtils.getLength(fromRanges);
+      char[] newSeqChars = new char[cdsWidth];
+
+      int newPos = 0;
+      for (int[] range : fromRanges)
       {
-        // reverse strand mapping - copy and complement one by one
-        for (int i = range[0]; i >= range[1]; i--)
+        if (range[0] <= range[1])
+        {
+          // forward strand mapping - just copy the range
+          int length = range[1] - range[0] + 1;
+          System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos,
+                  length);
+          newPos += length;
+        }
+        else
         {
-          newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]);
+          // reverse strand mapping - copy and complement one by one
+          for (int i = range[0]; i >= range[1]; i--)
+          {
+            newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]);
+          }
         }
       }
+
+      newSeq = new Sequence(seqId, newSeqChars, 1, newPos);
     }
 
-    /*
-     * assign 'from id' held in the mapping if set (e.g. EMBL protein_id),
-     * else generate a sequence name
-     */
-    String mapFromId = mapping.getMappedFromId();
-    String seqId = "CDS|" + (mapFromId != null ? mapFromId : seq.getName());
-    SequenceI newSeq = new Sequence(seqId, newSeqChars, 1, newPos);
     if (dataset != null)
     {
       SequenceI[] matches = dataset.findSequenceMatch(newSeq.getName());
@@ -2159,6 +2184,10 @@ public class AlignmentUtils
     {
       copyTo = copyTo.getDatasetSequence();
     }
+    if (fromSeq == copyTo || fromSeq.getDatasetSequence() == copyTo)
+    {
+      return 0; // shared dataset sequence
+    }
 
     /*
      * get features, optionally restricted by an ontology term
@@ -2412,23 +2441,24 @@ public class AlignmentUtils
   }
 
   /**
-   * Computes non-synonymous peptide variants from codon variants and adds them
-   * as sequence_variant features on the protein sequence (one feature per
-   * allele variant). Selected attributes (variant id, clinical significance)
-   * are copied over to the new features.
+   * Computes non-synonymous peptide variants from codon variants and adds them as
+   * sequence_variant features on the protein sequence (one feature per allele
+   * variant). Selected attributes (variant id, clinical significance) are copied
+   * over to the new features.
    * 
    * @param peptide
-   *          the protein sequence
+   *                        the protein dataset (ungapped) sequence
    * @param peptidePos
-   *          the position to compute peptide variants for
+   *                        the position to compute peptide variants for
    * @param codonVariants
-   *          a list of dna variants per codon position
+   *                        a list of dna variants per codon position
    * @return the number of features added
    */
   static int computePeptideVariants(SequenceI peptide, int peptidePos,
           List<DnaVariant>[] codonVariants)
   {
-    String residue = String.valueOf(peptide.getCharAt(peptidePos - 1));
+    String residue = String
+            .valueOf(peptide.getCharAt(peptidePos - peptide.getStart()));
     int count = 0;
     String base1 = codonVariants[0].get(0).base;
     String base2 = codonVariants[1].get(0).base;
@@ -2879,10 +2909,10 @@ public class AlignmentUtils
    * true; else returns false
    * 
    * @param unaligned
-   *          - sequences to be aligned based on aligned
+   *                    - sequences to be aligned based on aligned
    * @param aligned
-   *          - 'guide' alignment containing sequences derived from same dataset
-   *          as unaligned
+   *                    - 'guide' alignment containing sequences derived from same
+   *                    dataset as unaligned
    * @return
    */
   static boolean alignAsSameSequences(AlignmentI unaligned,
@@ -2906,15 +2936,22 @@ public class AlignmentUtils
     }
 
     /*
-     * first pass - check whether all sequences to be aligned share a dataset
-     * sequence with an aligned sequence
+     * first pass - check whether all sequences to be aligned share a 
+     * dataset sequence with an aligned sequence; also note the leftmost
+     * ungapped column from which to copy
      */
+    int leftmost = Integer.MAX_VALUE;
     for (SequenceI seq : unaligned.getSequences())
     {
-      if (!alignedDatasets.containsKey(seq.getDatasetSequence()))
+      final SequenceI ds = seq.getDatasetSequence();
+      if (!alignedDatasets.containsKey(ds))
       {
         return false;
       }
+      SequenceI alignedSeq = alignedDatasets.get(ds)
+              .get(0);
+      int startCol = alignedSeq.findIndex(seq.getStart()); // 1..
+      leftmost = Math.min(leftmost, startCol);
     }
 
     /*
@@ -2922,13 +2959,25 @@ public class AlignmentUtils
      * heuristic rule: pair off sequences in order for the case where 
      * more than one shares the same dataset sequence 
      */
+    final char gapCharacter = aligned.getGapCharacter();
     for (SequenceI seq : unaligned.getSequences())
     {
       List<SequenceI> alignedSequences = alignedDatasets
               .get(seq.getDatasetSequence());
-      // TODO: getSequenceAsString() will be deprecated in the future
-      // TODO: need to leave to SequenceI implementor to update gaps
-      seq.setSequence(alignedSequences.get(0).getSequenceAsString());
+      SequenceI alignedSeq = alignedSequences.get(0);
+
+      /*
+       * gap fill for leading (5') UTR if any
+       */
+      // TODO this copies intron columns - wrong!
+      int startCol = alignedSeq.findIndex(seq.getStart()); // 1..
+      int endCol = alignedSeq.findIndex(seq.getEnd());
+      char[] seqchars = new char[endCol - leftmost + 1];
+      Arrays.fill(seqchars, gapCharacter);
+      char[] toCopy = alignedSeq.getSequence(startCol - 1, endCol);
+      System.arraycopy(toCopy, 0, seqchars, startCol - leftmost,
+              toCopy.length);
+      seq.setSequence(String.valueOf(seqchars));
       if (alignedSequences.size() > 0)
       {
         // pop off aligned sequences (except the last one)
@@ -2936,6 +2985,12 @@ public class AlignmentUtils
       }
     }
 
+    /*
+     * finally remove gapped columns (e.g. introns)
+     */
+    new RemoveGapColCommand("", unaligned.getSequencesArray(), 0,
+            unaligned.getWidth() - 1, unaligned);
+
     return true;
   }
 
index 8545e94..0aa77fa 100644 (file)
@@ -202,7 +202,7 @@ public class FeatureDistanceModel extends DistanceScoreModel
          */
         Set<String> types = new HashSet<>();
         List<SequenceFeature> sfs = fr.findFeaturesAtResidue(
-                seq.getRefSeq(), spos);
+                seq.getRefSeq(), spos, spos);
         for (SequenceFeature sf : sfs)
         {
           types.add(sf.getType());
index 404c497..8aa2858 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.api;
 
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcherSetI;
@@ -161,15 +162,18 @@ public interface FeatureRenderer
   List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column);
 
   /**
-   * Returns features at the specified residue position on the given sequence.
-   * Non-positional features are not included.
+   * Returns features at the specified residue positions on the given sequence.
+   * Non-positional features are not included. Features are returned in render
+   * order of their feature type (last is on top). Within feature type, ordering
+   * is undefined.
    * 
    * @param sequence
-   * @param resNo
-   *          residue position (start..)
+   * @param fromResNo
+   * @param toResNo
    * @return
    */
-  List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence, int resNo);
+  List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
+          int fromResNo, int toResNo);
 
   /**
    * get current displayed types, in ordering of rendering (on top last)
@@ -280,4 +284,17 @@ public interface FeatureRenderer
    * @return
    */
   boolean isVisible(SequenceFeature feature);
+
+  /**
+   * Answers a bean containing a mapping, and a list of visible features in this
+   * alignment at a position (or range) which is mappable from the given sequence
+   * residue position in a mapped alignment. Features are returned in render order
+   * of feature type (on top last), with order within feature type undefined. If
+   * no features or mapping are found, answers null.
+   * 
+   * @param sequence
+   * @param pos
+   * @return
+   */
+  MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, int pos);
 }
index 2b554ea..a348300 100644 (file)
@@ -24,6 +24,13 @@ import java.awt.Color;
 
 public interface ViewStyleI
 {
+  void setShowComplementFeatures(boolean b);
+
+  boolean isShowComplementFeatures();
+
+  void setShowComplementFeaturesOnTop(boolean b);
+
+  boolean isShowComplementFeaturesOnTop();
 
   void setColourAppliesToAllGroups(boolean b);
 
index eb3be88..3bb5fe8 100644 (file)
@@ -1448,12 +1448,13 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       features = formatter.printJalviewFormat(
               viewport.getAlignment().getSequencesArray(),
-              alignPanel.getFeatureRenderer(), true);
+              alignPanel.getFeatureRenderer(), true, false);
     }
     else
     {
       features = formatter.printGffFormat(viewport.getAlignment()
-              .getSequencesArray(), alignPanel.getFeatureRenderer(), true);
+              .getSequencesArray(), alignPanel.getFeatureRenderer(), true,
+              false);
     }
 
     if (displayTextbox)
index fee68c8..2c07153 100644 (file)
@@ -744,7 +744,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   }
 
   @Override
-  public void highlightSequence(SearchResultsI results)
+  public String highlightSequence(SearchResultsI results)
   {
     if (av.isFollowHighlight())
     {
@@ -761,7 +761,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
     setStatusMessage(results);
     seqCanvas.highlightSearchResults(results);
-
+    return null;
   }
 
   @Override
index ec11fc1..fffa137 100644 (file)
@@ -116,7 +116,7 @@ public class AlignedCodonFrame
    */
   public AlignedCodonFrame()
   {
-    mappings = new ArrayList<SequenceToSequenceMapping>();
+    mappings = new ArrayList<>();
   }
 
   /**
@@ -179,7 +179,7 @@ public class AlignedCodonFrame
   {
     // TODO return a list instead?
     // return dnaSeqs;
-    List<SequenceI> seqs = new ArrayList<SequenceI>();
+    List<SequenceI> seqs = new ArrayList<>();
     for (SequenceToSequenceMapping ssm : mappings)
     {
       seqs.add(ssm.fromSeq);
@@ -190,7 +190,7 @@ public class AlignedCodonFrame
   public SequenceI[] getAaSeqs()
   {
     // TODO not used - remove?
-    List<SequenceI> seqs = new ArrayList<SequenceI>();
+    List<SequenceI> seqs = new ArrayList<>();
     for (SequenceToSequenceMapping ssm : mappings)
     {
       seqs.add(ssm.mapping.to);
@@ -200,7 +200,7 @@ public class AlignedCodonFrame
 
   public MapList[] getdnaToProt()
   {
-    List<MapList> maps = new ArrayList<MapList>();
+    List<MapList> maps = new ArrayList<>();
     for (SequenceToSequenceMapping ssm : mappings)
     {
       maps.add(ssm.mapping.map);
@@ -210,7 +210,7 @@ public class AlignedCodonFrame
 
   public Mapping[] getProtMappings()
   {
-    List<Mapping> maps = new ArrayList<Mapping>();
+    List<Mapping> maps = new ArrayList<>();
     for (SequenceToSequenceMapping ssm : mappings)
     {
       maps.add(ssm.mapping);
@@ -220,7 +220,7 @@ public class AlignedCodonFrame
 
   /**
    * Returns the first mapping found which is to or from the given sequence, or
-   * null.
+   * null if none is found
    * 
    * @param seq
    * @return
@@ -485,7 +485,7 @@ public class AlignedCodonFrame
   {
     MapList ml = null;
     SequenceI dnaSeq = null;
-    List<char[]> result = new ArrayList<char[]>();
+    List<char[]> result = new ArrayList<>();
 
     for (SequenceToSequenceMapping ssm : mappings)
     {
@@ -524,8 +524,8 @@ public class AlignedCodonFrame
    */
   public List<Mapping> getMappingsFromSequence(SequenceI seq)
   {
-    List<Mapping> result = new ArrayList<Mapping>();
-    List<SequenceI> related = new ArrayList<SequenceI>();
+    List<Mapping> result = new ArrayList<>();
+    List<SequenceI> related = new ArrayList<>();
     SequenceI seqDs = seq.getDatasetSequence();
     seqDs = seqDs != null ? seqDs : seq;
 
diff --git a/src/jalview/datamodel/MappedFeatures.java b/src/jalview/datamodel/MappedFeatures.java
new file mode 100644 (file)
index 0000000..3f43355
--- /dev/null
@@ -0,0 +1,236 @@
+package jalview.datamodel;
+
+import jalview.io.gff.Gff3Helper;
+import jalview.schemes.ResidueProperties;
+import jalview.util.MappingUtils;
+import jalview.util.StringUtils;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A data bean to hold a list of mapped sequence features (e.g. CDS features
+ * mapped from protein), and the mapping between the sequences. It also provides
+ * a method to derive peptide variants from codon variants.
+ * 
+ * @author gmcarstairs
+ */
+public class MappedFeatures
+{
+  private static final String HGV_SP = "HGVSp";
+
+  private static final String CSQ = "CSQ";
+
+  /*
+   * the mapping from one sequence to another
+   */
+  public final Mapping mapping;
+
+  /**
+   * the sequence mapped from
+   */
+  public final SequenceI fromSeq;
+
+  /*
+   * features on the sequence mapped to that overlap the mapped positions
+   */
+  public final List<SequenceFeature> features;
+
+  /*
+   * the residue position in the sequence mapped to
+   */
+  private final int toPosition;
+
+  /*
+   * the residue at toPosition 
+   */
+  private final char toResidue;
+
+  /*
+   * if the mapping is 3:1 or 1:3 (peptide to CDS), this holds the
+   * mapped positions i.e. codon base positions in CDS; to
+   * support calculation of peptide variants from alleles
+   */
+  private final int[] codonPos;
+
+  private final char[] baseCodon;
+
+  /**
+   * Constructor
+   * 
+   * @param theMapping
+   * @param from
+   *                      the sequence mapped from (e.g. CDS)
+   * @param pos
+   *                      the residue position in the sequence mapped to
+   * @param res
+   *                      the residue character at position pos
+   * @param theFeatures
+   *                      list of mapped features found in the 'from' sequence at
+   *                      the mapped position(s)
+   */
+  public MappedFeatures(Mapping theMapping, SequenceI from, int pos,
+          char res, List<SequenceFeature> theFeatures)
+  {
+    mapping = theMapping;
+    fromSeq = from;
+    toPosition = pos;
+    toResidue = res;
+    features = theFeatures;
+
+    /*
+     * determine codon positions and canonical codon
+     * for a peptide-to-CDS mapping
+     */
+    int[] codonIntervals = mapping.getMap().locateInFrom(toPosition, toPosition);
+    if (codonIntervals != null)
+    {
+      codonPos = MappingUtils.flattenRanges(codonIntervals);
+      if (codonPos.length == 3)
+      {
+        baseCodon = new char[3];
+        int cdsStart = fromSeq.getStart();
+        baseCodon[0] = fromSeq.getCharAt(codonPos[0] - cdsStart);
+        baseCodon[1] = fromSeq.getCharAt(codonPos[1] - cdsStart);
+        baseCodon[2] = fromSeq.getCharAt(codonPos[2] - cdsStart);
+      }
+      else
+      {
+        baseCodon = null;
+      }
+    }
+    else
+    {
+      codonPos = null;
+      baseCodon = null; // todo tidy!
+    }
+  }
+
+  /**
+   * Computes and returns comma-delimited HGVS notation peptide variants derived
+   * from codon allele variants. If no variants are found, answers an empty
+   * string.
+   * 
+   * @param sf
+   *             a sequence feature (which must be one of those held in this
+   *             object)
+   * @return
+   */
+  public String findProteinVariants(SequenceFeature sf)
+  {
+    if (!features.contains(sf) || baseCodon == null)
+    {
+      return "";
+    }
+
+    /*
+     * VCF data may already contain the protein consequence
+     */
+    String hgvsp = sf.getValueAsString(CSQ, HGV_SP);
+    if (hgvsp != null)
+    {
+      int colonPos = hgvsp.lastIndexOf(':');
+      if (colonPos >= 0)
+      {
+        String var = hgvsp.substring(colonPos + 1);
+        return var;
+      }
+    }
+
+    /*
+     * otherwise, compute codon and peptide variant
+     */
+    // todo avoid duplication of code in AlignmentUtils.buildDnaVariantsMap
+    int cdsPos = sf.getBegin();
+    if (cdsPos != sf.getEnd())
+    {
+      // not handling multi-locus variant features
+      return "";
+    }
+    if (cdsPos != codonPos[0] && cdsPos != codonPos[1]
+            && cdsPos != codonPos[2])
+    {
+      // e.g. feature on intron within spliced codon!
+      return "";
+    }
+
+    String alls = (String) sf.getValue(Gff3Helper.ALLELES);
+    if (alls == null)
+    {
+      return "";
+    }
+
+    String from3 = StringUtils.toSentenceCase(
+            ResidueProperties.aa2Triplet.get(String.valueOf(toResidue)));
+
+    /*
+     * make a peptide variant for each SNP allele 
+     * e.g. C,G,T gives variants G and T for base C
+     */
+    Set<String> variantPeptides = new HashSet<>();
+    String[] alleles = alls.toUpperCase().split(",");
+    StringBuilder vars = new StringBuilder();
+
+    for (String allele : alleles)
+    {
+      allele = allele.trim().toUpperCase();
+      if (allele.length() > 1 || "-".equals(allele))
+      {
+        continue; // multi-locus variant
+      }
+      char[] variantCodon = new char[3];
+      variantCodon[0] = baseCodon[0];
+      variantCodon[1] = baseCodon[1];
+      variantCodon[2] = baseCodon[2];
+
+      /*
+       * poke variant base into canonical codon;
+       * ignore first 'allele' (canonical base)
+       */
+      final int i = cdsPos == codonPos[0] ? 0
+              : (cdsPos == codonPos[1] ? 1 : 2);
+      variantCodon[i] = allele.toUpperCase().charAt(0);
+      if (variantCodon[i] == baseCodon[i])
+      {
+        continue;
+      }
+      String codon = new String(variantCodon);
+      String peptide = ResidueProperties.codonTranslate(codon);
+      boolean synonymous = toResidue == peptide.charAt(0);
+      StringBuilder var = new StringBuilder();
+      if (synonymous)
+      {
+        /*
+         * synonymous variant notation e.g. c.1062C>A(p.=)
+         */
+        var.append("c.").append(String.valueOf(cdsPos))
+                .append(String.valueOf(baseCodon[i])).append(">")
+                .append(String.valueOf(variantCodon[i]))
+                .append("(p.=)");
+      }
+      else
+      {
+        /*
+         * missense variant notation e.g. p.Arg355Met
+         */
+        String to3 = ResidueProperties.STOP.equals(peptide) ? "Ter"
+                : StringUtils.toSentenceCase(
+                        ResidueProperties.aa2Triplet.get(peptide));
+        var.append("p.").append(from3).append(String.valueOf(toPosition))
+                .append(to3);
+      }
+      if (!variantPeptides.contains(peptide)) // duplicate consequence
+      {
+        variantPeptides.add(peptide);
+        if (vars.length() > 0)
+        {
+          vars.append(",");
+        }
+        vars.append(var);
+      }
+    }
+
+    return vars.toString();
+  }
+}
index 7052f34..c8a7def 100755 (executable)
@@ -345,6 +345,11 @@ public class SequenceFeature implements FeatureLocationI
     return featureGroup;
   }
 
+  /**
+   * Adds a hyperlink for the feature. This should have the format label|url.
+   * 
+   * @param labelLink
+   */
   public void addLink(String labelLink)
   {
     if (links == null)
@@ -602,9 +607,11 @@ public class SequenceFeature implements FeatureLocationI
   /**
    * Answers an html-formatted report of feature details
    * 
+   * @param seqName
+   * 
    * @return
    */
-  public String getDetailsReport()
+  public String getDetailsReport(String seqName)
   {
     FeatureSourceI metadata = FeatureSources.getInstance()
             .getSource(source);
@@ -612,9 +619,10 @@ public class SequenceFeature implements FeatureLocationI
     StringBuilder sb = new StringBuilder(128);
     sb.append("<br>");
     sb.append("<table>");
+    sb.append(String.format(ROW_DATA, "Location", seqName,
+            begin == end ? begin
+                    : begin + (isContactFeature() ? ":" : "-") + end));
     sb.append(String.format(ROW_DATA, "Type", type, ""));
-    sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
-            : begin + (isContactFeature() ? ":" : "-") + end, ""));
     String desc = StringUtils.stripHtmlTags(description);
     sb.append(String.format(ROW_DATA, "Description", desc, ""));
     if (!Float.isNaN(score) && score != 0f)
index ba8396a..db2f0e1 100644 (file)
@@ -25,7 +25,6 @@ import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -204,7 +203,9 @@ public class SequenceFeatures implements SequenceFeaturesI
 
   /**
    * A convenience method that converts a vararg for feature types to an
-   * Iterable over matched feature sets in key order
+   * Iterable over matched feature sets. If no types are specified, all feature
+   * sets are returned. If one or more types are specified, feature sets for
+   * those types are returned, preserving the order of the types.
    * 
    * @param type
    * @return
@@ -220,12 +221,11 @@ public class SequenceFeatures implements SequenceFeaturesI
     }
 
     List<FeatureStore> types = new ArrayList<>();
-    List<String> args = Arrays.asList(type);
-    for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
+    for (String theType : type)
     {
-      if (args.contains(featureType.getKey()))
+      if (theType != null && featureStore.containsKey(theType))
       {
-        types.add(featureType.getValue());
+        types.add(featureStore.get(theType));
       }
     }
     return types;
index ca0283e..7213cba 100644 (file)
@@ -42,7 +42,8 @@ public interface SequenceFeaturesI
   /**
    * Returns a (possibly empty) list of features, optionally restricted to
    * specified types, which overlap the given (inclusive) sequence position
-   * range
+   * range. If types are specified, features are returned in the order of the
+   * types given.
    * 
    * @param from
    * @param to
index 5dc701d..001e18e 100644 (file)
@@ -330,8 +330,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
          * copy exon features to protein, compute peptide variants from dna 
          * variants and add as features on the protein sequence ta-da
          */
-        AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq,
-                mapList);
+        // JAL-3187 render on the fly instead
+        // AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, mapList);
       }
     } catch (Exception e)
     {
index c0a1e0d..1ceabd1 100644 (file)
@@ -49,6 +49,7 @@ import java.util.BitSet;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.StringTokenizer;
 import java.util.Vector;
 
 import org.jmol.adapter.smarter.SmarterJmolAdapter;
@@ -64,6 +65,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
         implements JmolStatusListener, JmolSelectionListener,
         ComponentListener
 {
+  private String lastMessage;
+
   boolean allChainsSelected = false;
 
   /*
@@ -88,8 +91,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   String lastCommand;
 
-  String lastMessage;
-
   boolean loadedInline;
 
   StringBuffer resetLastRes = new StringBuffer();
@@ -753,7 +754,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     viewer.openStringInline(string);
   }
 
-  public void mouseOverStructure(int atomIndex, String strInfo)
+  protected void mouseOverStructure(int atomIndex, final String strInfo)
   {
     int pdbResNum;
     int alocsep = strInfo.indexOf("^");
@@ -834,18 +835,36 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       } catch (Exception e)
       {
       }
-      ;
     }
-    if (lastMessage == null || !lastMessage.equals(strInfo))
+
+    /*
+     * highlight position on alignment(s); if some text is returned, 
+     * show this as a second line on the structure hover tooltip
+     */
+    String label = getSsm().mouseOverStructure(pdbResNum, chainId,
+            pdbfilename);
+    if (label != null)
     {
-      getSsm().mouseOverStructure(pdbResNum, chainId, pdbfilename);
+      // change comma to pipe separator (newline token for Jmol)
+      label = label.replace(',', '|');
+      StringTokenizer toks = new StringTokenizer(strInfo, " ");
+      StringBuilder sb = new StringBuilder();
+      sb.append("select ").append(String.valueOf(pdbResNum)).append(":")
+              .append(chainId).append("/1");
+      sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ")
+              .append(toks.nextToken());
+      sb.append("|").append(label).append("\"");
+      evalStateCommand(sb.toString());
     }
-
-    lastMessage = strInfo;
   }
 
   public void notifyAtomHovered(int atomIndex, String strInfo, String data)
   {
+    if (strInfo.equals(lastMessage))
+    {
+      return;
+    }
+    lastMessage = strInfo;
     if (data != null)
     {
       System.err.println("Ignoring additional hover info: " + data
index dad8511..3caaac3 100644 (file)
@@ -26,8 +26,10 @@ import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.gui.Desktop;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureMappingcommandSet;
@@ -104,7 +106,7 @@ public class ChimeraCommands
      * delimited). If length limit issues arise, refactor to return one color
      * command per colour.
      */
-    List<String> commands = new ArrayList<String>();
+    List<String> commands = new ArrayList<>();
     StringBuilder sb = new StringBuilder(256);
     boolean firstColour = true;
     for (Object key : colourMap.keySet())
@@ -196,7 +198,7 @@ public class ChimeraCommands
     AlignViewportI viewport = viewPanel.getAlignViewport();
     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
     AlignmentI al = viewport.getAlignment();
-    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<Object, AtomSpecModel>();
+    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
     Color lastColour = null;
 
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
@@ -257,7 +259,7 @@ public class ChimeraCommands
               {
                 if (startPos != -1)
                 {
-                  addColourRange(colourMap, lastColour, pdbfnum, startPos,
+                  addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
                           lastPos, lastChain);
                 }
                 startPos = pos;
@@ -269,7 +271,7 @@ public class ChimeraCommands
             // final colour range
             if (lastColour != null)
             {
-              addColourRange(colourMap, lastColour, pdbfnum, startPos,
+              addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
                       lastPos, lastChain);
             }
             // break;
@@ -281,26 +283,32 @@ public class ChimeraCommands
   }
 
   /**
-   * Helper method to add one contiguous colour range to the colour map.
+   * Helper method to add one contiguous range to the AtomSpec model for the given
+   * value (creating the model if necessary). As used by Jalview, {@code value} is
+   * <ul>
+   * <li>a colour, when building a 'colour structure by sequence' command</li>
+   * <li>a feature value, when building a 'set Chimera attributes from features'
+   * command</li>
+   * </ul>
    * 
    * @param map
-   * @param key
+   * @param value
    * @param model
    * @param startPos
    * @param endPos
    * @param chain
    */
-  protected static void addColourRange(Map<Object, AtomSpecModel> map,
-          Object key, int model, int startPos, int endPos, String chain)
+  protected static void addAtomSpecRange(Map<Object, AtomSpecModel> map,
+          Object value, int model, int startPos, int endPos, String chain)
   {
     /*
      * Get/initialize map of data for the colour
      */
-    AtomSpecModel atomSpec = map.get(key);
+    AtomSpecModel atomSpec = map.get(value);
     if (atomSpec == null)
     {
       atomSpec = new AtomSpecModel();
-      map.put(key, atomSpec);
+      map.put(value, atomSpec);
     }
 
     atomSpec.addRange(model, startPos, endPos, chain);
@@ -349,7 +357,7 @@ public class ChimeraCommands
           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
           AlignmentViewPanel viewPanel)
   {
-    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
+    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
 
     FeatureRenderer fr = viewPanel.getFeatureRenderer();
     if (fr == null)
@@ -357,8 +365,27 @@ public class ChimeraCommands
       return theMap;
     }
 
+    AlignViewportI viewport = viewPanel.getAlignViewport();
     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
-    if (visibleFeatures.isEmpty())
+
+    /*
+     * if alignment is showing features from complement, we also transfer
+     * these features to the corresponding mapped structure residues
+     */
+    boolean showLinkedFeatures = viewport.isShowComplementFeatures();
+    List<String> complementFeatures = new ArrayList<>();
+    FeatureRenderer complementRenderer = null;
+    if (showLinkedFeatures)
+    {
+      AlignViewportI comp = fr.getViewport().getCodingComplement();
+      if (comp != null)
+      {
+        complementRenderer = Desktop.getAlignFrameFor(comp)
+                .getFeatureRenderer();
+        complementFeatures = complementRenderer.getDisplayedFeatureTypes();
+      }
+    }
+    if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
     {
       return theMap;
     }
@@ -379,16 +406,23 @@ public class ChimeraCommands
         {
           final SequenceI seq = seqs[pdbfnum][seqNo];
           int sp = alignment.findIndex(seq);
-          if (mapping[m].getSequence() == seq && sp > -1)
+          StructureMapping structureMapping = mapping[m];
+          if (structureMapping.getSequence() == seq && sp > -1)
           {
             /*
              * found a sequence with a mapping to a structure;
              * now scan its features
              */
-            SequenceI asp = alignment.getSequenceAt(sp);
-
-            scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
-                    pdbfnum);
+            if (!visibleFeatures.isEmpty())
+            {
+              scanSequenceFeatures(visibleFeatures, structureMapping, seq,
+                      theMap, pdbfnum);
+            }
+            if (showLinkedFeatures)
+            {
+              scanComplementFeatures(complementRenderer, structureMapping,
+                      seq, theMap, pdbfnum);
+            }
           }
         }
       }
@@ -397,9 +431,85 @@ public class ChimeraCommands
   }
 
   /**
-   * Inspect features on the sequence; for each feature that is visible,
-   * determine its mapped ranges in the structure (if any) according to the
-   * given mapping, and add them to the map
+   * Scans visible features in mapped positions of the CDS/peptide complement, and
+   * adds any found to the map of attribute values/structure positions
+   * 
+   * @param complementRenderer
+   * @param structureMapping
+   * @param seq
+   * @param theMap
+   * @param modelNumber
+   */
+  protected static void scanComplementFeatures(
+          FeatureRenderer complementRenderer,
+          StructureMapping structureMapping, SequenceI seq,
+          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  {
+    /*
+     * for each sequence residue mapped to a structure position...
+     */
+    for (int seqPos : structureMapping.getMapping().keySet())
+    {
+      /*
+       * find visible complementary features at mapped position(s)
+       */
+      MappedFeatures mf = complementRenderer
+              .findComplementFeaturesAtResidue(seq, seqPos);
+      if (mf != null)
+      {
+        for (SequenceFeature sf : mf.features)
+        {
+          String type = sf.getType();
+
+          /*
+           * Don't copy features which originated from Chimera
+           */
+          if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+                  .equals(sf.getFeatureGroup()))
+          {
+            continue;
+          }
+
+          /*
+           * record feature 'value' (score/description/type) as at the
+           * corresponding structure position
+           */
+          List<int[]> mappedRanges = structureMapping
+                  .getPDBResNumRanges(seqPos, seqPos);
+
+          if (!mappedRanges.isEmpty())
+          {
+            String value = sf.getDescription();
+            if (value == null || value.length() == 0)
+            {
+              value = type;
+            }
+            float score = sf.getScore();
+            if (score != 0f && !Float.isNaN(score))
+            {
+              value = Float.toString(score);
+            }
+            Map<Object, AtomSpecModel> featureValues = theMap.get(type);
+            if (featureValues == null)
+            {
+              featureValues = new HashMap<>();
+              theMap.put(type, featureValues);
+            }
+            for (int[] range : mappedRanges)
+            {
+              addAtomSpecRange(featureValues, value, modelNumber, range[0],
+                      range[1], structureMapping.getChain());
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Inspect features on the sequence; for each feature that is visible, determine
+   * its mapped ranges in the structure (if any) according to the given mapping,
+   * and add them to the map.
    * 
    * @param visibleFeatures
    * @param mapping
@@ -418,15 +528,14 @@ public class ChimeraCommands
       String type = sf.getType();
 
       /*
-       * Only copy visible features, don't copy any which originated
-       * from Chimera, and suppress uninteresting ones (e.g. RESNUM)
+       * Don't copy features which originated from Chimera
        */
-      boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-              .equals(sf.getFeatureGroup());
-      if (isFromViewer)
+      if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+              .equals(sf.getFeatureGroup()))
       {
         continue;
       }
+
       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
               sf.getEnd());
 
@@ -445,12 +554,12 @@ public class ChimeraCommands
         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
         if (featureValues == null)
         {
-          featureValues = new HashMap<Object, AtomSpecModel>();
+          featureValues = new HashMap<>();
           theMap.put(type, featureValues);
         }
         for (int[] range : mappedRanges)
         {
-          addColourRange(featureValues, value, modelNumber, range[0],
+          addAtomSpecRange(featureValues, value, modelNumber, range[0],
                   range[1], mapping.getChain());
         }
       }
@@ -475,7 +584,7 @@ public class ChimeraCommands
   protected static List<String> buildSetAttributeCommands(
           Map<String, Map<Object, AtomSpecModel>> featureMap)
   {
-    List<String> commands = new ArrayList<String>();
+    List<String> commands = new ArrayList<>();
     for (String featureType : featureMap.keySet())
     {
       String attributeName = makeAttributeName(featureType);
index fac531e..0b0916d 100644 (file)
@@ -30,17 +30,17 @@ import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
 import jalview.util.MessageManager;
 
-import java.awt.BorderLayout;
 import java.awt.Color;
-import java.awt.FlowLayout;
+import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.io.FileWriter;
 import java.io.PrintWriter;
 
-import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
 import javax.swing.ButtonGroup;
 import javax.swing.JButton;
+import javax.swing.JCheckBox;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JLayeredPane;
@@ -71,6 +71,29 @@ public class AnnotationExporter extends JPanel
 
   private boolean wholeView;
 
+  /*
+   * option to export linked (CDS/peptide) features when shown 
+   * on the alignment, converted to this alignment's coordinates
+   */
+  private JCheckBox includeLinkedFeatures;
+
+  /*
+   * output format option shown for feature export
+   */
+  JRadioButton GFFFormat = new JRadioButton();
+
+  /*
+   * output format option shown for annotation export
+   */
+  JRadioButton CSVFormat = new JRadioButton();
+
+  private JPanel linkedFeaturesPanel;
+
+  /**
+   * Constructor
+   * 
+   * @param panel
+   */
   public AnnotationExporter(AlignmentPanel panel)
   {
     this.ap = panel;
@@ -85,17 +108,25 @@ public class AnnotationExporter extends JPanel
     frame = new JInternalFrame();
     frame.setContentPane(this);
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
-    Desktop.addInternalFrame(frame, "", frame.getPreferredSize().width,
-            frame.getPreferredSize().height);
+    Dimension preferredSize = frame.getPreferredSize();
+    Desktop.addInternalFrame(frame, "", true, preferredSize.width,
+            preferredSize.height, true, true);
   }
 
   /**
-   * Configures the diglog for options to export visible features
+   * Configures the dialog for options to export visible features. If from a split
+   * frame panel showing linked features, make the option to include these in the
+   * export visible.
    */
   public void exportFeatures()
   {
     exportFeatures = true;
     CSVFormat.setVisible(false);
+    if (ap.av.isShowComplementFeatures())
+    {
+      linkedFeaturesPanel.setVisible(true);
+      frame.pack();
+    }
     frame.setTitle(MessageManager.getString("label.export_features"));
   }
 
@@ -215,14 +246,17 @@ public class AnnotationExporter extends JPanel
 
     FeaturesFile formatter = new FeaturesFile();
     final FeatureRenderer fr = ap.getFeatureRenderer();
+    boolean includeComplement = includeLinkedFeatures.isSelected();
+
     if (GFFFormat.isSelected())
     {
-      text = formatter.printGffFormat(sequences, fr, includeNonPositional);
+      text = formatter.printGffFormat(sequences, fr, includeNonPositional,
+              includeComplement);
     }
     else
     {
       text = formatter.printJalviewFormat(sequences, fr,
-              includeNonPositional);
+              includeNonPositional, includeComplement);
     }
     return text;
   }
@@ -268,11 +302,70 @@ public class AnnotationExporter extends JPanel
     }
   }
 
+  /**
+   * Adds widgets to the panel
+   * 
+   * @throws Exception
+   */
   private void jbInit() throws Exception
   {
-    this.setLayout(new BorderLayout());
+    this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+    this.setBackground(Color.white);
+
+    JPanel formatPanel = buildFormatOptionsPanel();
+    JPanel linkedFeatures = buildLinkedFeaturesPanel();
+    JPanel actionsPanel = buildActionsPanel();
+
+    this.add(formatPanel);
+    this.add(linkedFeatures);
+    this.add(actionsPanel);
+  }
+
+  /**
+   * Builds a panel with a checkbox for the option to export linked (CDS/peptide)
+   * features. This is hidden by default, and only made visible if exporting
+   * features from a split frame panel which is configured to show linked
+   * features.
+   * 
+   * @return
+   */
+  private JPanel buildLinkedFeaturesPanel()
+  {
+    linkedFeaturesPanel = new JPanel();
+    linkedFeaturesPanel.setOpaque(false);
+
+    boolean nucleotide = ap.av.isNucleotide();
+    String complement = nucleotide
+            ? MessageManager.getString("label.protein").toLowerCase()
+            : "CDS";
+    JLabel label = new JLabel(
+            MessageManager.formatMessage("label.include_linked_features",
+                    complement));
+    label.setHorizontalAlignment(SwingConstants.TRAILING);
+    String tooltip = MessageManager
+            .formatMessage("label.include_linked_tooltip", complement);
+    label.setToolTipText(
+            JvSwingUtils.wrapTooltip(true, tooltip));
+
+    includeLinkedFeatures = new JCheckBox();
+    linkedFeaturesPanel.add(label);
+    linkedFeaturesPanel.add(includeLinkedFeatures);
+    linkedFeaturesPanel.setVisible(false);
+
+    return linkedFeaturesPanel;
+  }
+
+  /**
+   * Builds the panel with to File or Textbox or Close actions
+   * 
+   * @return
+   */
+  JPanel buildActionsPanel()
+  {
+    JPanel actionsPanel = new JPanel();
+    actionsPanel.setOpaque(false);
 
-    toFile.setText(MessageManager.getString("label.to_file"));
+    JButton toFile = new JButton(MessageManager.getString("label.to_file"));
     toFile.addActionListener(new ActionListener()
     {
       @Override
@@ -281,7 +374,8 @@ public class AnnotationExporter extends JPanel
         toFile_actionPerformed();
       }
     });
-    toTextbox.setText(MessageManager.getString("label.to_textbox"));
+    JButton toTextbox = new JButton(
+            MessageManager.getString("label.to_textbox"));
     toTextbox.addActionListener(new ActionListener()
     {
       @Override
@@ -290,7 +384,7 @@ public class AnnotationExporter extends JPanel
         toTextbox_actionPerformed();
       }
     });
-    close.setText(MessageManager.getString("action.close"));
+    JButton close = new JButton(MessageManager.getString("action.close"));
     close.addActionListener(new ActionListener()
     {
       @Override
@@ -299,52 +393,49 @@ public class AnnotationExporter extends JPanel
         close_actionPerformed();
       }
     });
+
+    actionsPanel.add(toFile);
+    actionsPanel.add(toTextbox);
+    actionsPanel.add(close);
+
+    return actionsPanel;
+  }
+
+  /**
+   * Builds the panel with options to output in Jalview, GFF or CSV format. GFF is
+   * only made visible when exporting features, CSV only when exporting
+   * annotation.
+   * 
+   * @return
+   */
+  JPanel buildFormatOptionsPanel()
+  {
+    JPanel formatPanel = new JPanel();
+    // formatPanel.setBorder(BorderFactory.createEtchedBorder());
+    formatPanel.setOpaque(false);
+
+    JRadioButton jalviewFormat = new JRadioButton("Jalview");
     jalviewFormat.setOpaque(false);
     jalviewFormat.setSelected(true);
-    jalviewFormat.setText("Jalview");
     GFFFormat.setOpaque(false);
     GFFFormat.setText("GFF");
     CSVFormat.setOpaque(false);
     CSVFormat.setText(MessageManager.getString("label.csv_spreadsheet"));
-    jLabel1.setHorizontalAlignment(SwingConstants.TRAILING);
-    jLabel1.setText(MessageManager.getString("action.format") + " ");
-    this.setBackground(Color.white);
-    jPanel3.setBorder(BorderFactory.createEtchedBorder());
-    jPanel3.setOpaque(false);
-    jPanel1.setOpaque(false);
-    jPanel1.add(toFile);
-    jPanel1.add(toTextbox);
-    jPanel1.add(close);
-    jPanel3.add(jLabel1);
-    jPanel3.add(jalviewFormat);
-    jPanel3.add(GFFFormat);
-    jPanel3.add(CSVFormat);
+
+    ButtonGroup buttonGroup = new ButtonGroup();
     buttonGroup.add(jalviewFormat);
     buttonGroup.add(GFFFormat);
     buttonGroup.add(CSVFormat);
-    this.add(jPanel3, BorderLayout.CENTER);
-    this.add(jPanel1, BorderLayout.SOUTH);
-  }
-
-  JPanel jPanel1 = new JPanel();
-
-  JButton toFile = new JButton();
-
-  JButton toTextbox = new JButton();
-
-  JButton close = new JButton();
 
-  ButtonGroup buttonGroup = new ButtonGroup();
+    JLabel format = new JLabel(
+            MessageManager.getString("action.format") + " ");
+    format.setHorizontalAlignment(SwingConstants.TRAILING);
 
-  JRadioButton jalviewFormat = new JRadioButton();
+    formatPanel.add(format);
+    formatPanel.add(jalviewFormat);
+    formatPanel.add(GFFFormat);
+    formatPanel.add(CSVFormat);
 
-  JRadioButton GFFFormat = new JRadioButton();
-
-  JRadioButton CSVFormat = new JRadioButton();
-
-  JLabel jLabel1 = new JLabel();
-
-  JPanel jPanel3 = new JPanel();
-
-  FlowLayout flowLayout1 = new FlowLayout();
+    return formatPanel;
+  }
 }
index 9ad6596..92947e2 100644 (file)
@@ -39,6 +39,7 @@ import jalview.util.DBRefUtils;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 import jalview.ws.SequenceFetcher;
 
 import java.util.ArrayList;
@@ -177,17 +178,17 @@ public class CrossRefAction implements Runnable
               .isShowSequenceFeatures();
       newFrame.setShowSeqFeatures(showSequenceFeatures);
       copyThis.setShowSeqFeatures(showSequenceFeatures);
-      FeatureRenderer myFeatureStyling = alignFrame.alignPanel
+      FeatureRendererModel myFeatureStyling = alignFrame.alignPanel
               .getSeqPanel().seqCanvas.getFeatureRenderer();
 
       /*
        * copy feature rendering settings to split frame
        */
-      FeatureRenderer fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas
+      FeatureRendererModel fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas
               .getFeatureRenderer();
       fr1.transferSettings(myFeatureStyling);
       fr1.findAllFeatures(true);
-      FeatureRenderer fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas
+      FeatureRendererModel fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas
               .getFeatureRenderer();
       fr2.transferSettings(myFeatureStyling);
       fr2.findAllFeatures(true);
index 9ca409b..441dca7 100644 (file)
  */
 package jalview.gui;
 
+import jalview.api.AlignViewportI;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
+import jalview.api.ViewStyleI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcher;
@@ -35,6 +37,7 @@ import jalview.schemes.FeatureColour;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
+import jalview.viewmodel.styles.ViewStyle;
 import jalview.xml.binding.jalview.JalviewUserColours;
 import jalview.xml.binding.jalview.JalviewUserColours.Colour;
 import jalview.xml.binding.jalview.JalviewUserColours.Filter;
@@ -44,6 +47,7 @@ import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.FlowLayout;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
@@ -143,6 +147,8 @@ public class FeatureSettings extends JPanel
 
   private float originalTransparency;
 
+  private ViewStyleI originalViewStyle;
+
   private Map<String, FeatureMatcherSetI> originalFilters;
 
   final JInternalFrame frame;
@@ -190,6 +196,7 @@ public class FeatureSettings extends JPanel
     transparency.setMaximum(100 - originalTransparencyAsPercent);
 
     originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
+    originalViewStyle = new ViewStyle(af.viewport.getViewStyle());
 
     try
     {
@@ -290,6 +297,7 @@ public class FeatureSettings extends JPanel
           boolean extendSelection = evt.isShiftDown();
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
                   invertSelection, extendSelection, toggleSelection, type);
+          fr.ap.av.sendSelection();
         }
       }
 
@@ -398,8 +406,6 @@ public class FeatureSettings extends JPanel
           final Object typeCol, final Map<String, float[][]> minmax, int x,
           int y)
   {
-    final FeatureColourI featureColour = (FeatureColourI) typeCol;
-
     JPopupMenu men = new JPopupMenu(MessageManager
             .formatMessage("label.settings_for_param", new String[]
             { type }));
@@ -444,6 +450,7 @@ public class FeatureSettings extends JPanel
       {
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
                 false, type);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem clearCols = new JMenuItem(MessageManager
@@ -455,6 +462,7 @@ public class FeatureSettings extends JPanel
       {
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
                 false, type);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem hideCols = new JMenuItem(
@@ -465,6 +473,7 @@ public class FeatureSettings extends JPanel
       public void actionPerformed(ActionEvent arg0)
       {
         fr.ap.alignFrame.hideFeatureColumns(type, true);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem hideOtherCols = new JMenuItem(
@@ -475,6 +484,7 @@ public class FeatureSettings extends JPanel
       public void actionPerformed(ActionEvent arg0)
       {
         fr.ap.alignFrame.hideFeatureColumns(type, false);
+        fr.ap.av.sendSelection();
       }
     });
     men.add(selCols);
@@ -538,7 +548,7 @@ public class FeatureSettings extends JPanel
       {
         fr.setGroupVisibility(check.getText(), check.isSelected());
         resetTable(new String[] { grp });
-        af.alignPanel.paintAlignment(true, true);
+        refreshDisplay();
       }
     });
     groupPanel.add(check);
@@ -1096,7 +1106,7 @@ public class FeatureSettings extends JPanel
 
     if (fr.setFeaturePriority(rowData, visibleNew))
     {
-      af.alignPanel.paintAlignment(true, true);
+      refreshDisplay();
     }
   }
 
@@ -1205,6 +1215,7 @@ public class FeatureSettings extends JPanel
         fr.setTransparency(originalTransparency);
         fr.setFeatureFilters(originalFilters);
         updateFeatureRenderer(originalData);
+        af.getViewport().setViewStyle(originalViewStyle);
         close();
       }
     });
@@ -1255,7 +1266,7 @@ public class FeatureSettings extends JPanel
         if (!inConstruction)
         {
           fr.setTransparency((100 - transparency.getValue()) / 100f);
-          af.alignPanel.paintAlignment(true, true);
+          refreshDisplay();
         }
       }
     });
@@ -1264,8 +1275,42 @@ public class FeatureSettings extends JPanel
     transparency.setToolTipText(
             MessageManager.getString("label.transparency_tip"));
 
-    JPanel transPanel = new JPanel(new GridLayout(1, 2));
-    bigPanel.add(transPanel, BorderLayout.SOUTH);
+    boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
+    String text = MessageManager.formatMessage("label.show_linked_features",
+            nucleotide
+                    ? MessageManager.getString("label.protein")
+                            .toLowerCase()
+                    : "CDS");
+    JCheckBox showComplement = new JCheckBox(text);
+    showComplement.setSelected(af.getViewport().isShowComplementFeatures());
+    showComplement.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        af.getViewport()
+                .setShowComplementFeatures(showComplement.isSelected());
+        refreshDisplay();
+      }
+    });
+
+    JCheckBox showComplementOnTop = new JCheckBox(
+            MessageManager.getString("label.on_top"));
+    showComplementOnTop
+            .setSelected(af.getViewport().isShowComplementFeaturesOnTop());
+    showComplementOnTop.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        af.getViewport().setShowComplementFeaturesOnTop(
+                showComplementOnTop.isSelected());
+        refreshDisplay();
+      }
+    });
+
+    JPanel lowerPanel = new JPanel(new GridLayout(1, 2));
+    bigPanel.add(lowerPanel, BorderLayout.SOUTH);
 
     JPanel transbuttons = new JPanel(new GridLayout(5, 1));
     transbuttons.add(optimizeOrder);
@@ -1273,8 +1318,21 @@ public class FeatureSettings extends JPanel
     transbuttons.add(sortByScore);
     transbuttons.add(sortByDens);
     transbuttons.add(help);
-    transPanel.add(transparency);
-    transPanel.add(transbuttons);
+
+    boolean hasComplement = af.getViewport().getCodingComplement() != null;
+    JPanel transPanelLeft = new JPanel(
+            new GridLayout(hasComplement ? 3 : 2, 1));
+    transPanelLeft.add(new JLabel(" Colour transparency" + ":"));
+    transPanelLeft.add(transparency);
+    if (hasComplement)
+    {
+      JPanel cp = new JPanel(new FlowLayout(FlowLayout.LEFT));
+      cp.add(showComplement);
+      cp.add(showComplementOnTop);
+      transPanelLeft.add(cp);
+    }
+    lowerPanel.add(transPanelLeft);
+    lowerPanel.add(transbuttons);
 
     JPanel buttonPanel = new JPanel();
     buttonPanel.add(ok);
@@ -1288,11 +1346,27 @@ public class FeatureSettings extends JPanel
   }
 
   /**
+   * Repaints alignment, structure and overview (if shown). If there is a
+   * complementary view which is showing this view's features, then also
+   * repaints that.
+   */
+  void refreshDisplay()
+  {
+    af.alignPanel.paintAlignment(true, true);
+    AlignViewportI complement = af.getViewport().getCodingComplement();
+    if (complement != null && complement.isShowComplementFeatures())
+    {
+      AlignFrame af2 = Desktop.getAlignFrameFor(complement);
+      af2.alignPanel.paintAlignment(true, true);
+    }
+  }
+
+  /**
    * Answers a suitable tooltip to show on the colour cell of the table
    * 
    * @param fcol
    * @param withHint
-   *          if true include 'click to edit' and similar text
+   *                   if true include 'click to edit' and similar text
    * @return
    */
   public static String getColorTooltip(FeatureColourI fcol,
@@ -1429,13 +1503,22 @@ public class FeatureSettings extends JPanel
     }
 
     /**
-     * Answers the class of the object in column c of the first row of the table
+     * Answers the class of column c of the table
      */
     @Override
     public Class<?> getColumnClass(int c)
     {
-      Object v = getValueAt(0, c);
-      return v == null ? null : v.getClass();
+      switch (c)
+      {
+      case TYPE_COLUMN:
+        return String.class;
+      case COLOUR_COLUMN:
+        return FeatureColour.class;
+      case FILTER_COLUMN:
+        return FeatureMatcherSet.class;
+      default:
+        return Boolean.class;
+      }
     }
 
     @Override
index 7456e18..5b77dfc 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.gui;
 
+import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureColourI;
 import jalview.bin.Cache;
@@ -674,7 +675,7 @@ public class FeatureTypeSettings extends JalviewDialog
          */
         if (ap != null)
         {
-          ap.paintAlignment(true, true);
+          refreshDisplay(true);
         }
       }
     });
@@ -875,7 +876,7 @@ public class FeatureTypeSettings extends JalviewDialog
      * save the colour, and repaint stuff
      */
     fr.setColour(featureType, acg);
-    ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
+    refreshDisplay(updateStructsAndOverview);
 
     updateColoursTab();
   }
@@ -1014,7 +1015,7 @@ public class FeatureTypeSettings extends JalviewDialog
   {
     fr.setColour(featureType, originalColour);
     fr.setFeatureFilter(featureType, originalFilter);
-    ap.paintAlignment(true, true);
+    refreshDisplay(true);
   }
 
   /**
@@ -1753,8 +1754,26 @@ public class FeatureTypeSettings extends JalviewDialog
      * (note this might now be an empty filter with no conditions)
      */
     fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
-    ap.paintAlignment(true, true);
+    refreshDisplay(true);
 
     updateFiltersTab();
   }
+
+  /**
+   * Repaints alignment, structure and overview (if shown). If there is a
+   * complementary view which is showing this view's features, then also
+   * repaints that.
+   * 
+   * @param updateStructsAndOverview
+   */
+  void refreshDisplay(boolean updateStructsAndOverview)
+  {
+    ap.paintAlignment(true, updateStructsAndOverview);
+    AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
+    if (complement != null && complement.isShowComplementFeatures())
+    {
+      AlignFrame af2 = Desktop.getAlignFrameFor(complement);
+      af2.alignPanel.paintAlignment(true, updateStructsAndOverview);
+    }
+  }
 }
index d11d7a1..2b1507a 100755 (executable)
@@ -22,7 +22,6 @@ package jalview.gui;
 
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.gui.SeqPanel.MousePos;
@@ -369,28 +368,12 @@ public class IdPanel extends JPanel
     }
 
     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex);
-
-    /*
-     *  build a new links menu based on the current links
-     *  and any non-positional features
-     */
-    List<SequenceFeature> features = null;
     if (sq != null)
     {
-    List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-      features = sq.getFeatures().getNonPositionalFeatures();
-    for (SequenceFeature sf : features)
-    {
-      if (sf.links != null)
-      {
-        nlinks.addAll(sf.links);
-      }
-    }
+      PopupMenu pop = new PopupMenu(alignPanel, sq,
+              Preferences.getGroupURLLinks());
+      pop.show(this, e.getX(), e.getY());
     }
-
-    PopupMenu pop = new PopupMenu(alignPanel, sq, features,
-            Preferences.getGroupURLLinks());
-    pop.show(this, e.getX(), e.getY());
   }
 
   /**
index 2f11c30..9d63c6a 100644 (file)
@@ -27,6 +27,7 @@ import jalview.datamodel.SequenceI;
 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 import jalview.io.DataSourceType;
 import jalview.structure.StructureSelectionManager;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import javax.swing.SwingUtilities;
 
@@ -43,7 +44,7 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding
   }
 
   @Override
-  public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment)
+  public FeatureRendererModel getFeatureRenderer(AlignmentViewPanel alignment)
   {
     AlignmentPanel ap = (alignment == null) ? cvf.getAlignmentPanel()
             : (AlignmentPanel) alignment;
index 89088b8..0f49381 100644 (file)
@@ -25,6 +25,7 @@ import jalview.bin.Cache;
 import jalview.renderer.OverviewRenderer;
 import jalview.renderer.OverviewResColourFinder;
 import jalview.viewmodel.OverviewDimensions;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.Color;
 import java.awt.Dimension;
@@ -132,7 +133,7 @@ public class OverviewCanvas extends JComponent
    *          the renderer to transfer feature colouring from
    */
   public void draw(boolean showSequenceFeatures, boolean showAnnotation,
-          FeatureRenderer transferRenderer)
+          FeatureRendererModel transferRenderer)
   {
     miniMe = null;
 
index 702773b..415054e 100644 (file)
@@ -24,15 +24,16 @@ import jalview.analysis.AAFrequency;
 import jalview.analysis.AlignmentAnnotationUtils;
 import jalview.analysis.AlignmentUtils;
 import jalview.analysis.Conservation;
+import jalview.api.AlignViewportI;
 import jalview.bin.Cache;
 import jalview.commands.ChangeCaseCommand;
 import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
@@ -47,11 +48,13 @@ import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemes;
 import jalview.schemes.PIDColourScheme;
 import jalview.schemes.ResidueColourScheme;
+import jalview.util.Comparison;
 import jalview.util.GroupUrlLink;
 import jalview.util.GroupUrlLink.UrlStringTooLongException;
 import jalview.util.MessageManager;
 import jalview.util.StringUtils;
 import jalview.util.UrlLink;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.Color;
 import java.awt.event.ActionEvent;
@@ -65,6 +68,7 @@ import java.util.Hashtable;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.Vector;
@@ -83,6 +87,19 @@ import javax.swing.JRadioButtonMenuItem;
  */
 public class PopupMenu extends JPopupMenu implements ColourChangeListener
 {
+  /*
+   * true for ID Panel menu, false for alignment panel menu
+   */
+  private final boolean forIdPanel;
+
+  private final AlignmentPanel ap;
+
+  /*
+   * the sequence under the cursor when clicked
+   * (additional sequences may be selected)
+   */
+  private final SequenceI sequence;
+
   JMenu groupMenu = new JMenu();
 
   JMenuItem groupName = new JMenuItem();
@@ -97,28 +114,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
   protected JMenuItem modifyConservation = new JMenuItem();
 
-  AlignmentPanel ap;
-
   JMenu sequenceMenu = new JMenu();
 
-  JMenuItem sequenceName = new JMenuItem();
-
-  JMenuItem sequenceDetails = new JMenuItem();
-
-  JMenuItem sequenceSelDetails = new JMenuItem();
-
   JMenuItem makeReferenceSeq = new JMenuItem();
 
-  JMenuItem chooseAnnotations = new JMenuItem();
-
-  SequenceI sequence;
-
   JMenuItem createGroupMenuItem = new JMenuItem();
 
   JMenuItem unGroupMenuItem = new JMenuItem();
 
-  JMenuItem outline = new JMenuItem();
-
   JMenu colourMenu = new JMenu();
 
   JCheckBoxMenuItem showBoxes = new JCheckBoxMenuItem();
@@ -131,18 +134,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
   JMenu editMenu = new JMenu();
 
-  JMenuItem cut = new JMenuItem();
-
-  JMenuItem copy = new JMenuItem();
-
   JMenuItem upperCase = new JMenuItem();
 
   JMenuItem lowerCase = new JMenuItem();
 
   JMenuItem toggle = new JMenuItem();
 
-  JMenu pdbMenu = new JMenu();
-
   JMenu outputMenu = new JMenu();
 
   JMenu seqShowAnnotationsMenu = new JMenu();
@@ -159,22 +156,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   JMenuItem groupAddReferenceAnnotations = new JMenuItem(
           MessageManager.getString("label.add_reference_annotations"));
 
-  JMenuItem sequenceFeature = new JMenuItem();
-
   JMenuItem textColour = new JMenuItem();
 
-  JMenu jMenu1 = new JMenu();
+  JMenu editGroupMenu = new JMenu();
 
-  JMenuItem pdbStructureDialog = new JMenuItem();
+  JMenuItem chooseStructure = new JMenuItem();
 
   JMenu rnaStructureMenu = new JMenu();
 
-  JMenuItem editSequence = new JMenuItem();
-
-  JMenu groupLinksMenu;
-
-  JMenuItem hideInsertions = new JMenuItem();
-
   /**
    * Constructs a menu with sub-menu items for any hyperlinks for the sequence
    * and/or features provided. Hyperlinks may include a lookup by sequence id,
@@ -185,7 +174,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    * @param features
    * @return
    */
-  static JMenu buildLinkMenu(final SequenceI seq,
+  protected static JMenu buildLinkMenu(final SequenceI seq,
           List<SequenceFeature> features)
   {
     JMenu linkMenu = new JMenu(MessageManager.getString("action.link"));
@@ -357,41 +346,55 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   }
 
   /**
-   * Creates a new PopupMenu object.
+   * Constructor for a PopupMenu for a click in the alignment panel (on a residue)
    * 
    * @param ap
+   *              the panel in which the mouse is clicked
    * @param seq
-   * @param features
-   *          non-positional features (for seq not null), or positional features
-   *          at residue (for seq equal to null)
+   *              the sequence under the mouse
+   * @throws NullPointerException
+   *                                if seq is null
    */
-  public PopupMenu(final AlignmentPanel ap, SequenceI seq,
-          List<SequenceFeature> features)
+  public PopupMenu(final AlignmentPanel ap, SequenceI seq, int column)
   {
-    this(ap, seq, features, null);
+    this(false, ap, seq, column, null);
   }
 
   /**
-   * Constructor
+   * Constructor for a PopupMenu for a click in the sequence id panel
    * 
    * @param alignPanel
+   *                     the panel in which the mouse is clicked
    * @param seq
-   *          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
+   *                     the sequence under the mouse click
    * @param groupLinks
+   *                     templates for sequence external links
+   * @throws NullPointerException
+   *                                if seq is null
    */
   public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq,
-          List<SequenceFeature> features, List<String> groupLinks)
+          List<String> groupLinks)
+  {
+    this(true, alignPanel, seq, -1, groupLinks);
+  }
+
+  /**
+   * Private constructor that constructs a popup menu for either sequence ID
+   * Panel, or alignment context
+   * 
+   * @param fromIdPanel
+   * @param alignPanel
+   * @param seq
+   * @param column
+   *                      aligned column position (0...)
+   * @param groupLinks
+   */
+  private PopupMenu(boolean fromIdPanel,
+          final AlignmentPanel alignPanel,
+          final SequenceI seq, final int column, List<String> groupLinks)
   {
-    // /////////////////////////////////////////////////////////
-    // If this is activated from the sequence panel, the user may want to
-    // edit or annotate a particular residue. Therefore display the residue menu
-    //
-    // If from the IDPanel, we must display the sequence menu
-    // ////////////////////////////////////////////////////////
+    Objects.requireNonNull(seq);
+    this.forIdPanel = fromIdPanel;
     this.ap = alignPanel;
     sequence = seq;
 
@@ -416,9 +419,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
      * 'reference annotations' that may be added to the alignment. First for the
      * currently selected sequence (if there is one):
      */
-    final List<SequenceI> selectedSequence = (seq == null
-            ? Collections.<SequenceI> emptyList()
-            : Arrays.asList(seq));
+    final List<SequenceI> selectedSequence = (forIdPanel && seq != null
+            ? Arrays.asList(seq)
+            : Collections.<SequenceI> emptyList());
     buildAnnotationTypesMenus(seqShowAnnotationsMenu,
             seqHideAnnotationsMenu, selectedSequence);
     configureReferenceAnnotationsMenu(seqAddReferenceAnnotations,
@@ -443,9 +446,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       e.printStackTrace();
     }
 
-    JMenuItem menuItem;
-    if (seq != null)
+    if (forIdPanel)
     {
+      JMenuItem menuItem;
       sequenceMenu.setText(sequence.getName());
       if (seq == alignPanel.av.getAlignment().getSeqrep())
       {
@@ -600,7 +603,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
       if (addOption)
       {
-        menuItem = new JMenuItem(
+        JMenuItem menuItem = new JMenuItem(
                 MessageManager.getString("action.reveal_all"));
         menuItem.addActionListener(new ActionListener()
         {
@@ -692,27 +695,57 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     {
       createGroupMenuItem.setVisible(true);
       unGroupMenuItem.setVisible(false);
-      jMenu1.setText(MessageManager.getString("action.edit_new_group"));
+      editGroupMenu.setText(MessageManager.getString("action.edit_new_group"));
     }
     else
     {
       createGroupMenuItem.setVisible(false);
       unGroupMenuItem.setVisible(true);
-      jMenu1.setText(MessageManager.getString("action.edit_group"));
+      editGroupMenu.setText(MessageManager.getString("action.edit_group"));
     }
 
-    if (seq == null)
+    if (!forIdPanel)
     {
       sequenceMenu.setVisible(false);
-      pdbStructureDialog.setVisible(false);
+      chooseStructure.setVisible(false);
       rnaStructureMenu.setVisible(false);
     }
 
+    addLinksAndFeatures(seq, column);
+  }
+
+  /**
+   * Adds
+   * <ul>
+   * <li>configured sequence database links (ID panel popup menu)</li>
+   * <li>non-positional feature links (ID panel popup menu)</li>
+   * <li>positional feature links (alignment panel popup menu)</li>
+   * <li>feature details links (alignment panel popup menu)</li>
+   * </ul>
+   * If this panel is also showed complementary (CDS/protein) features, then links
+   * to their feature details are also added.
+   * 
+   * @param seq
+   * @param column
+   */
+  void addLinksAndFeatures(final SequenceI seq, final int column)
+  {
+    List<SequenceFeature> features = null;
+    if (forIdPanel)
+    {
+      features = sequence.getFeatures().getNonPositionalFeatures();
+    }
+    else
+    {
+      features = ap.getFeatureRenderer().findFeaturesAtColumn(sequence,
+              column + 1);
+    }
+
     addLinks(seq, features);
 
-    if (seq == null)
+    if (!forIdPanel)
     {
-      addFeatureDetails(features);
+      addFeatureDetails(features, seq, column);
     }
   }
 
@@ -720,75 +753,127 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    * Add a link to show feature details for each sequence feature
    * 
    * @param features
+   * @param column
+   * @param seq
    */
-  protected void addFeatureDetails(List<SequenceFeature> features)
+  protected void addFeatureDetails(List<SequenceFeature> features,
+          SequenceI seq, int column)
   {
-    if (features == null || features.isEmpty())
+    /*
+     * add features in CDS/protein complement at the corresponding
+     * position if configured to do so
+     */
+    MappedFeatures mf = null;
+    if (ap.av.isShowComplementFeatures())
+    {
+      if (!Comparison.isGap(sequence.getCharAt(column)))
+      {
+        AlignViewportI complement = ap.getAlignViewport()
+                .getCodingComplement();
+        AlignFrame af = Desktop.getAlignFrameFor(complement);
+        FeatureRendererModel fr2 = af.getFeatureRenderer();
+        int seqPos = sequence.findPosition(column);
+        mf = fr2.findComplementFeaturesAtResidue(sequence, seqPos);
+      }
+    }
+
+    if (features.isEmpty() && mf == null)
     {
+      /*
+       * no features to show at this position
+       */
       return;
     }
+
     JMenu details = new JMenu(
             MessageManager.getString("label.feature_details"));
     add(details);
 
+    String name = seq.getName();
     for (final SequenceFeature sf : features)
     {
-      int start = sf.getBegin();
-      int end = sf.getEnd();
-      String desc = null;
-      if (start == end)
+      addFeatureDetailsMenuItem(details, name, sf);
+    }
+
+    if (mf != null)
+    {
+      name = mf.fromSeq == seq ? mf.mapping.getTo().getName()
+              : mf.fromSeq.getName();
+      for (final SequenceFeature sf : mf.features)
       {
-        desc = String.format("%s %d", sf.getType(), start);
+        addFeatureDetailsMenuItem(details, name, sf);
       }
-      else
+    }
+  }
+
+  /**
+   * A helper method to add one menu item whose action is to show details for one
+   * feature
+   * 
+   * @param details
+   * @param seqName
+   * @param sf
+   */
+  void addFeatureDetailsMenuItem(JMenu details, final String seqName,
+          final SequenceFeature sf)
+  {
+    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);
+    }
+    String tooltip = desc;
+    String description = sf.getDescription();
+    if (description != null)
+    {
+      description = StringUtils.stripHtmlTags(description);
+      if (description.length() > 12)
       {
-        desc = String.format("%s %d-%d", sf.getType(), start, end);
+        desc = desc + " " + description.substring(0, 12) + "..";
       }
-      String tooltip = desc;
-      String description = sf.getDescription();
-      if (description != null)
+      else
       {
-        description = StringUtils.stripHtmlTags(description);
-        if (description.length() > 12)
-        {
-          desc = desc + " " + description.substring(0, 12) + "..";
-        }
-        else
-        {
-          desc = desc + " " + description;
-        }
-        tooltip = tooltip + " " + description;
+        desc = desc + " " + description;
       }
-      if (sf.getFeatureGroup() != null)
+      tooltip = tooltip + " " + description;
+    }
+    if (sf.getFeatureGroup() != null)
+    {
+      tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")");
+    }
+    JMenuItem item = new JMenuItem(desc);
+    item.setToolTipText(tooltip);
+    item.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
       {
-        tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")");
+        showFeatureDetails(seqName, sf);
       }
-      JMenuItem item = new JMenuItem(desc);
-      item.setToolTipText(tooltip);
-      item.addActionListener(new ActionListener()
-      {
-        @Override
-        public void actionPerformed(ActionEvent e)
-        {
-          showFeatureDetails(sf);
-        }
-      });
-      details.add(item);
-    }
+    });
+    details.add(item);
   }
 
   /**
    * Opens a panel showing a text report of feature dteails
    * 
+   * @param seqName
+   * 
    * @param sf
    */
-  protected void showFeatureDetails(SequenceFeature sf)
+  protected void showFeatureDetails(String seqName, SequenceFeature sf)
   {
     CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
-    // it appears Java's CSS does not support border-collaps :-(
+    // it appears Java's CSS does not support border-collapse :-(
     cap.addStylesheetRule("table { border-collapse: collapse;}");
     cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
-    cap.setText(sf.getDetailsReport());
+    cap.setText(sf.getDetailsReport(seqName));
 
     Desktop.addInternalFrame(cap,
             MessageManager.getString("label.feature_details"), 500, 500);
@@ -806,12 +891,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    */
   void addLinks(final SequenceI seq, List<SequenceFeature> features)
   {
-    JMenu linkMenu = buildLinkMenu(seq, features);
+    JMenu linkMenu = buildLinkMenu(forIdPanel ? seq : null, features);
 
     // only add link menu if it has entries
     if (linkMenu.getItemCount() > 0)
     {
-      if (sequence != null)
+      if (forIdPanel)
       {
         sequenceMenu.add(linkMenu);
       }
@@ -959,7 +1044,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     // menu appears asap
     // sequence only URLs
     // ID/regex match URLs
-    groupLinksMenu = new JMenu(
+    JMenu groupLinksMenu = new JMenu(
             MessageManager.getString("action.group_link"));
     // three types of url that might be created.
     JMenu[] linkMenus = new JMenu[] { null,
@@ -1118,7 +1203,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     });
     sequenceMenu.setText(MessageManager.getString("label.sequence"));
-    sequenceName.setText(
+
+    JMenuItem sequenceName = new JMenuItem(
             MessageManager.getString("label.edit_name_description"));
     sequenceName.addActionListener(new ActionListener()
     {
@@ -1128,8 +1214,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         sequenceName_actionPerformed();
       }
     });
-    chooseAnnotations
-            .setText(MessageManager.getString("action.choose_annotations"));
+    JMenuItem chooseAnnotations = new JMenuItem(
+            MessageManager.getString("action.choose_annotations"));
     chooseAnnotations.addActionListener(new ActionListener()
     {
       @Override
@@ -1138,24 +1224,24 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         chooseAnnotations_actionPerformed(e);
       }
     });
-    sequenceDetails
-            .setText(MessageManager.getString("label.sequence_details"));
+    JMenuItem sequenceDetails = new JMenuItem(
+            MessageManager.getString("label.sequence_details"));
     sequenceDetails.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        sequenceDetails_actionPerformed();
+        createSequenceDetailsReport(new SequenceI[] { sequence });
       }
     });
-    sequenceSelDetails
-            .setText(MessageManager.getString("label.sequence_details"));
+    JMenuItem sequenceSelDetails = new JMenuItem(
+            MessageManager.getString("label.sequence_details"));
     sequenceSelDetails.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        sequenceSelectionDetails_actionPerformed();
+        createSequenceDetailsReport(ap.av.getSequenceSelection());
       }
     });
 
@@ -1180,7 +1266,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     });
 
-    outline.setText(MessageManager.getString("action.border_colour"));
+    JMenuItem outline = new JMenuItem(
+            MessageManager.getString("action.border_colour"));
     outline.addActionListener(new ActionListener()
     {
       @Override
@@ -1230,7 +1317,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     });
     editMenu.setText(MessageManager.getString("action.edit"));
-    cut.setText(MessageManager.getString("action.cut"));
+    JMenuItem cut = new JMenuItem(MessageManager.getString("action.cut"));
     cut.addActionListener(new ActionListener()
     {
       @Override
@@ -1248,7 +1335,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         changeCase(e);
       }
     });
-    copy.setText(MessageManager.getString("action.copy"));
+    JMenuItem copy = new JMenuItem(MessageManager.getString("action.copy"));
     copy.addActionListener(new ActionListener()
     {
       @Override
@@ -1285,7 +1372,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
             .setText(MessageManager.getString("label.show_annotations"));
     groupHideAnnotationsMenu
             .setText(MessageManager.getString("label.hide_annotations"));
-    sequenceFeature.setText(
+    JMenuItem sequenceFeature = new JMenuItem(
             MessageManager.getString("label.create_sequence_feature"));
     sequenceFeature.addActionListener(new ActionListener()
     {
@@ -1295,10 +1382,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         sequenceFeature_actionPerformed();
       }
     });
-    jMenu1.setText(MessageManager.getString("label.group"));
-    pdbStructureDialog.setText(
+    editGroupMenu.setText(MessageManager.getString("label.group"));
+    chooseStructure.setText(
             MessageManager.getString("label.show_pdbstruct_dialog"));
-    pdbStructureDialog.addActionListener(new ActionListener()
+    chooseStructure.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent actionEvent)
@@ -1316,7 +1403,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
             .setText(MessageManager.getString("label.view_rna_structure"));
 
     // colStructureMenu.setText("Colour By Structure");
-    editSequence.setText(
+    JMenuItem editSequence = new JMenuItem(
             MessageManager.getString("label.edit_sequence") + "...");
     editSequence.addActionListener(new ActionListener()
     {
@@ -1338,25 +1425,25 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
       }
     });
-    hideInsertions
-            .setText(MessageManager.getString("label.hide_insertions"));
-    hideInsertions.addActionListener(new ActionListener()
-    {
-
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        hideInsertions_actionPerformed(e);
-      }
-    });
 
     groupMenu.add(sequenceSelDetails);
     add(groupMenu);
     add(sequenceMenu);
     add(rnaStructureMenu);
-    add(pdbStructureDialog);
-    if (sequence != null)
+    add(chooseStructure);
+    if (forIdPanel)
     {
+      JMenuItem hideInsertions = new JMenuItem(
+              MessageManager.getString("label.hide_insertions"));
+      hideInsertions.addActionListener(new ActionListener()
+      {
+
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          hideInsertions_actionPerformed(e);
+        }
+      });
       add(hideInsertions);
     }
     // annotations configuration panel suppressed for now
@@ -1377,7 +1464,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     groupMenu.add(sequenceFeature);
     groupMenu.add(createGroupMenuItem);
     groupMenu.add(unGroupMenuItem);
-    groupMenu.add(jMenu1);
+    groupMenu.add(editGroupMenu);
     sequenceMenu.add(sequenceName);
     sequenceMenu.add(sequenceDetails);
     sequenceMenu.add(makeReferenceSeq);
@@ -1391,17 +1478,13 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     editMenu.add(upperCase);
     editMenu.add(lowerCase);
     editMenu.add(toggle);
-    // JBPNote: These shouldn't be added here - should appear in a generic
-    // 'apply web service to this sequence menu'
-    // pdbMenu.add(RNAFold);
-    // pdbMenu.add(ContraFold);
-    jMenu1.add(groupName);
-    jMenu1.add(colourMenu);
-    jMenu1.add(showBoxes);
-    jMenu1.add(showText);
-    jMenu1.add(showColourText);
-    jMenu1.add(outline);
-    jMenu1.add(displayNonconserved);
+    editGroupMenu.add(groupName);
+    editGroupMenu.add(colourMenu);
+    editGroupMenu.add(showBoxes);
+    editGroupMenu.add(showText);
+    editGroupMenu.add(showColourText);
+    editGroupMenu.add(outline);
+    editGroupMenu.add(displayNonconserved);
   }
 
   /**
@@ -1664,11 +1747,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     createSequenceDetailsReport(ap.av.getSequenceSelection());
   }
 
-  protected void sequenceDetails_actionPerformed()
-  {
-    createSequenceDetailsReport(new SequenceI[] { sequence });
-  }
-
   public void createSequenceDetailsReport(SequenceI[] sequences)
   {
     CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
@@ -1856,10 +1934,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Shows a dialog where the sequence name and description may be edited. If a
+   * name containing spaces is entered, these are converted to underscores, with a
+   * warning message.
    */
   void sequenceName_actionPerformed()
   {
@@ -1877,9 +1954,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       return;
     }
 
-    if (dialog.getName() != null)
+    String name = dialog.getName();
+    if (name != null)
     {
-      if (dialog.getName().indexOf(" ") > -1)
+      if (name.indexOf(" ") > -1)
       {
         JvOptionPane.showMessageDialog(ap,
                 MessageManager
@@ -1887,9 +1965,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 MessageManager
                         .getString("label.no_spaces_allowed_sequence_name"),
                 JvOptionPane.WARNING_MESSAGE);
+        name = name.replace(' ', '_');
       }
 
-      sequence.setName(dialog.getName().replace(' ', '_'));
+      sequence.setName(name);
       ap.paintAlignment(false, false);
     }
 
@@ -2105,38 +2184,20 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     }
   }
 
-  public void colourByStructure(String pdbid)
-  {
-    Annotation[] anots = ap.av.getStructureSelectionManager()
-            .colourSequenceFromStructure(sequence, pdbid);
-
-    AlignmentAnnotation an = new AlignmentAnnotation("Structure",
-            "Coloured by " + pdbid, anots);
-
-    ap.av.getAlignment().addAnnotation(an);
-    an.createSequenceMapping(sequence, 0, true);
-    // an.adjustForAlignment();
-    ap.av.getAlignment().setAnnotationIndex(an, 0);
-
-    ap.adjustAnnotationHeight();
-
-    sequence.addAlignmentAnnotation(an);
-
-  }
-
   public void editSequence_actionPerformed(ActionEvent actionEvent)
   {
     SequenceGroup sg = ap.av.getSelectionGroup();
 
+    SequenceI seq = sequence;
     if (sg != null)
     {
-      if (sequence == null)
+      if (seq == null)
       {
-        sequence = sg.getSequenceAt(0);
+        seq = sg.getSequenceAt(0);
       }
 
       EditNameDialog dialog = new EditNameDialog(
-              sequence.getSequenceAsString(sg.getStartRes(),
+              seq.getSequenceAsString(sg.getStartRes(),
                       sg.getEndRes() + 1),
               null, MessageManager.getString("label.edit_sequence"), null,
               MessageManager.getString("label.edit_sequence"),
index 1176df5..75bf0cc 100644 (file)
@@ -29,6 +29,7 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
@@ -50,6 +51,7 @@ import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.ViewportRanges;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -61,6 +63,7 @@ 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;
 
@@ -832,11 +835,11 @@ public class SeqPanel extends JPanel
    * the start of the highlighted region.
    */
   @Override
-  public void highlightSequence(SearchResultsI results)
+  public String highlightSequence(SearchResultsI results)
   {
     if (results == null || results.equals(lastSearchResults))
     {
-      return;
+      return null;
     }
     lastSearchResults = results;
 
@@ -862,6 +865,77 @@ public class SeqPanel extends JPanel
     {
       setStatusMessage(results);
     }
+    return results.isEmpty() ? null : getHighlightInfo(results);
+  }
+
+  /**
+   * temporary hack: answers a message suitable to show on structure hover
+   * label. This is normally null. It is a peptide variation description if
+   * <ul>
+   * <li>results are a single residue in a protein alignment</li>
+   * <li>there is a mapping to a coding sequence (codon)</li>
+   * <li>there are one or more SNP variant features on the codon</li>
+   * </ul>
+   * in which case the answer is of the format (e.g.) "p.Glu388Asp"
+   * 
+   * @param results
+   * @return
+   */
+  private String getHighlightInfo(SearchResultsI results)
+  {
+    /*
+     * ideally, just find mapped CDS (as we don't care about render style here);
+     * for now, go via split frame complement's FeatureRenderer
+     */
+    AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
+    if (complement == null)
+    {
+      return null;
+    }
+    AlignFrame af = Desktop.getAlignFrameFor(complement);
+    FeatureRendererModel fr2 = af.getFeatureRenderer();
+
+    int j = results.getSize();
+    List<String> infos = new ArrayList<>();
+    for (int i = 0; i < j; i++)
+    {
+      SearchResultMatchI match = results.getResults().get(i);
+      int pos = match.getStart();
+      if (pos == match.getEnd())
+      {
+        SequenceI seq = match.getSequence();
+        SequenceI ds = seq.getDatasetSequence() == null ? seq
+                : seq.getDatasetSequence();
+        MappedFeatures mf = fr2
+                .findComplementFeaturesAtResidue(ds, pos);
+        if (mf != null)
+        {
+          for (SequenceFeature sf : mf.features)
+          {
+            String pv = mf.findProteinVariants(sf);
+            if (pv.length() > 0 && !infos.contains(pv))
+            {
+              infos.add(pv);
+            }
+          }
+        }
+      }
+    }
+
+    if (infos.isEmpty())
+    {
+      return null;
+    }
+    StringBuilder sb = new StringBuilder();
+    for (String info : infos)
+    {
+      if (sb.length() > 0)
+      {
+        sb.append("|");
+      }
+      sb.append(info);
+    }
+    return sb.toString();
   }
 
   @Override
@@ -974,6 +1048,27 @@ public class SeqPanel extends JPanel
               .findFeaturesAtColumn(sequence, column + 1);
       seqARep.appendFeatures(tooltipText, pos, features,
               this.ap.getSeqPanel().seqCanvas.fr);
+
+      /*
+       * add features in CDS/protein complement at the corresponding
+       * position if configured to do so
+       */
+      if (av.isShowComplementFeatures())
+      {
+        if (!Comparison.isGap(sequence.getCharAt(column)))
+        {
+          AlignViewportI complement = ap.getAlignViewport()
+                  .getCodingComplement();
+          AlignFrame af = Desktop.getAlignFrameFor(complement);
+          FeatureRendererModel fr2 = af.getFeatureRenderer();
+          MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
+                  pos);
+          if (mf != null)
+          {
+            seqARep.appendFeatures(tooltipText, pos, mf, fr2);
+          }
+        }
+      }
     }
     if (tooltipText.length() == 6) // <html>
     {
@@ -2159,11 +2254,11 @@ public class SeqPanel extends JPanel
     final int column = pos.column;
     final int seq = pos.seqIndex;
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> features = ap.getFeatureRenderer()
-            .findFeaturesAtColumn(sequence, column + 1);
-
-    PopupMenu pop = new PopupMenu(ap, null, features);
-    pop.show(this, evt.getX(), evt.getY());
+    if (sequence != null)
+    {
+      PopupMenu pop = new PopupMenu(ap, sequence, column);
+      pop.show(this, evt.getX(), evt.getY());
+    }
   }
 
   /**
index 70f2ac7..a69788b 100755 (executable)
@@ -29,11 +29,13 @@ import jalview.api.FeaturesSourceI;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcherSet;
 import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.gui.Desktop;
 import jalview.io.gff.GffHelperBase;
 import jalview.io.gff.GffHelperFactory;
 import jalview.io.gff.GffHelperI;
@@ -49,9 +51,11 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 
 /**
  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
@@ -563,32 +567,31 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns contents of a Jalview format features file, for visible features,
-   * as filtered by type and group. Features with a null group are displayed if
-   * their feature type is visible. Non-positional features may optionally be
-   * included (with no check on type or group).
+   * Returns contents of a Jalview format features file, for visible features, as
+   * filtered by type and group. Features with a null group are displayed if their
+   * feature type is visible. Non-positional features may optionally be included
+   * (with no check on type or group).
    * 
    * @param sequences
    * @param fr
    * @param includeNonPositional
-   *          if true, include non-positional features (regardless of group or
-   *          type)
+   *                               if true, include non-positional features
+   *                               (regardless of group or type)
+   * @param includeComplement
+   *                               if true, include visible complementary
+   *                               (CDS/protein) positional features, with
+   *                               locations converted to local sequence
+   *                               coordinates
    * @return
    */
   public String printJalviewFormat(SequenceI[] sequences,
-          FeatureRenderer fr, boolean includeNonPositional)
+          FeatureRenderer fr, boolean includeNonPositional,
+          boolean includeComplement)
   {
     Map<String, FeatureColourI> visibleColours = fr
             .getDisplayedFeatureCols();
     Map<String, FeatureMatcherSetI> featureFilters = fr.getFeatureFilters();
 
-    if (!includeNonPositional
-            && (visibleColours == null || visibleColours.isEmpty()))
-    {
-      // no point continuing.
-      return "No Features Visible";
-    }
-
     /*
      * write out feature colours (if we know them)
      */
@@ -620,10 +623,151 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     int count = outputFeaturesByGroup(out, fr, types, sequences,
             includeNonPositional);
 
+    if (includeComplement)
+    {
+      count += outputComplementFeatures(out, fr, sequences);
+    }
+
     return count > 0 ? out.toString() : "No Features Visible";
   }
 
   /**
+   * Outputs any visible complementary (CDS/peptide) positional features as
+   * Jalview format, within feature group. The coordinates of the linked features
+   * are converted to the corresponding positions of the local sequences.
+   * 
+   * @param out
+   * @param fr
+   * @param sequences
+   * @return
+   */
+  private int outputComplementFeatures(StringBuilder out,
+          FeatureRenderer fr, SequenceI[] sequences)
+  {
+    AlignViewportI comp = fr.getViewport().getCodingComplement();
+    FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp)
+            .getFeatureRenderer();
+
+    /*
+     * bin features by feature group and sequence
+     */
+    Map<String, Map<String, List<SequenceFeature>>> map = new TreeMap<>(
+            String.CASE_INSENSITIVE_ORDER);
+    int count = 0;
+
+    for (SequenceI seq : sequences)
+    {
+      /*
+       * find complementary features
+       */
+      List<SequenceFeature> complementary = findComplementaryFeatures(seq,
+              fr2);
+      String seqName = seq.getName();
+
+      for (SequenceFeature sf : complementary)
+      {
+        String group = sf.getFeatureGroup();
+        if (!map.containsKey(group))
+        {
+          map.put(group, new LinkedHashMap<>()); // preserves sequence order
+        }
+        Map<String, List<SequenceFeature>> groupFeatures = map.get(group);
+        if (!groupFeatures.containsKey(seqName))
+        {
+          groupFeatures.put(seqName, new ArrayList<>());
+        }
+        List<SequenceFeature> foundFeatures = groupFeatures.get(seqName);
+        foundFeatures.add(sf);
+        count++;
+      }
+    }
+
+    /*
+     * output features by group
+     */
+    for (Entry<String, Map<String, List<SequenceFeature>>> groupFeatures : map.entrySet())
+    {
+      out.append(newline);
+      String group = groupFeatures.getKey();
+      if (!"".equals(group))
+      {
+        out.append(STARTGROUP).append(TAB).append(group).append(newline);
+      }
+      Map<String, List<SequenceFeature>> seqFeaturesMap = groupFeatures
+              .getValue();
+      for (Entry<String, List<SequenceFeature>> seqFeatures : seqFeaturesMap
+              .entrySet())
+      {
+        String sequenceName = seqFeatures.getKey();
+        for (SequenceFeature sf : seqFeatures.getValue())
+        {
+          formatJalviewFeature(out, sequenceName, sf);
+        }
+      }
+      if (!"".equals(group))
+      {
+        out.append(ENDGROUP).append(TAB).append(group).append(newline);
+      }
+    }
+
+    return count;
+  }
+
+  /**
+   * Answers a list of mapped features visible in the (CDS/protein) complement,
+   * with feature positions translated to local sequence coordinates
+   * 
+   * @param seq
+   * @param fr2
+   * @return
+   */
+  protected List<SequenceFeature> findComplementaryFeatures(SequenceI seq,
+          FeatureRenderer fr2)
+  {
+    /*
+     * avoid duplication of features (e.g. peptide feature 
+     * at all 3 mapped codon positions)
+     */
+    List<SequenceFeature> found = new ArrayList<>();
+    List<SequenceFeature> complementary = new ArrayList<>();
+
+    for (int pos = seq.getStart(); pos <= seq.getEnd(); pos++)
+    {
+      MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq, pos);
+
+      if (mf != null)
+      {
+        MapList mapping = mf.mapping.getMap();
+        for (SequenceFeature sf : mf.features)
+        {
+          /*
+           * make a virtual feature with local coordinates
+           */
+          if (!found.contains(sf))
+          {
+            String group = sf.getFeatureGroup();
+            if (group == null)
+            {
+              group = "";
+            }
+            found.add(sf);
+            int begin = sf.getBegin();
+            int end = sf.getEnd();
+            int[] range = mf.mapping.getTo() == seq.getDatasetSequence()
+                    ? mapping.locateInTo(begin, end)
+                    : mapping.locateInFrom(begin, end);
+            SequenceFeature sf2 = new SequenceFeature(sf, range[0],
+                    range[1], group, sf.getScore());
+            complementary.add(sf2);
+          }
+        }
+      }
+    }
+
+    return complementary;
+  }
+
+  /**
    * Outputs any feature filters defined for visible feature types, sandwiched by
    * STARTFILTERS and ENDFILTERS lines
    * 
@@ -745,7 +889,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
               }
             }
             firstInGroup = false;
-            out.append(formatJalviewFeature(sequenceName, sf));
+            formatJalviewFeature(out, sequenceName, sf);
           }
         }
       }
@@ -759,14 +903,16 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
+   * Formats one feature in Jalview format and appends to the string buffer
+   * 
    * @param out
    * @param sequenceName
    * @param sequenceFeature
    */
-  protected String formatJalviewFeature(
-          String sequenceName, SequenceFeature sequenceFeature)
+  protected void formatJalviewFeature(
+          StringBuilder out, String sequenceName,
+          SequenceFeature sequenceFeature)
   {
-    StringBuilder out = new StringBuilder(64);
     if (sequenceFeature.description == null
             || sequenceFeature.description.equals(""))
     {
@@ -791,7 +937,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
           if (sequenceFeature.description.indexOf(href) == -1)
           {
-            out.append(" <a href=\"" + href + "\">" + label + "</a>");
+            out.append(" <a href=\"").append(href).append("\">")
+                    .append(label).append("</a>");
           }
         }
 
@@ -816,8 +963,6 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       out.append(sequenceFeature.score);
     }
     out.append(newline);
-
-    return out.toString();
   }
 
   /**
@@ -876,34 +1021,40 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * Returns features output in GFF2 format
    * 
    * @param sequences
-   *          the sequences whose features are to be output
+   *                                       the sequences whose features are to be
+   *                                       output
    * @param visible
-   *          a map whose keys are the type names of visible features
+   *                                       a map whose keys are the type names of
+   *                                       visible features
    * @param visibleFeatureGroups
    * @param includeNonPositionalFeatures
+   * @param includeComplement
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
-          FeatureRenderer fr, boolean includeNonPositionalFeatures)
+          FeatureRenderer fr, boolean includeNonPositionalFeatures,
+          boolean includeComplement)
   {
+    FeatureRenderer fr2 = null;
+    if (includeComplement)
+    {
+      AlignViewportI comp = fr.getViewport().getCodingComplement();
+      fr2 = Desktop.getAlignFrameFor(comp).getFeatureRenderer();
+    }
+
     Map<String, FeatureColourI> visibleColours = fr.getDisplayedFeatureCols();
 
     StringBuilder out = new StringBuilder(256);
 
     out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
 
-    if (!includeNonPositionalFeatures
-            && (visibleColours == null || visibleColours.isEmpty()))
-    {
-      return out.toString();
-    }
-
     String[] types = visibleColours == null ? new String[0]
             : visibleColours.keySet()
                     .toArray(new String[visibleColours.keySet().size()]);
 
     for (SequenceI seq : sequences)
     {
+      List<SequenceFeature> seqFeatures = new ArrayList<>();
       List<SequenceFeature> features = new ArrayList<>();
       if (includeNonPositionalFeatures)
       {
@@ -913,51 +1064,29 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       {
         features.addAll(seq.getFeatures().getPositionalFeatures(types));
       }
-
       for (SequenceFeature sf : features)
       {
-        if (!sf.isNonPositional() && !fr.isVisible(sf))
+        if (sf.isNonPositional() || fr.isVisible(sf))
         {
           /*
-           * feature hidden by group visibility, colour threshold,
+           * drop features hidden by group visibility, colour threshold,
            * or feature filter condition
            */
-          continue;
-        }
-
-        String source = sf.featureGroup;
-        if (source == null)
-        {
-          source = sf.getDescription();
+          seqFeatures.add(sf);
         }
+      }
 
-        out.append(seq.getName());
-        out.append(TAB);
-        out.append(source);
-        out.append(TAB);
-        out.append(sf.type);
-        out.append(TAB);
-        out.append(sf.begin);
-        out.append(TAB);
-        out.append(sf.end);
-        out.append(TAB);
-        out.append(sf.score);
-        out.append(TAB);
-
-        int strand = sf.getStrand();
-        out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
-        out.append(TAB);
-
-        String phase = sf.getPhase();
-        out.append(phase == null ? "." : phase);
-
-        // miscellaneous key-values (GFF column 9)
-        String attributes = sf.getAttributes();
-        if (attributes != null)
-        {
-          out.append(TAB).append(attributes);
-        }
+      if (includeComplement)
+      {
+        seqFeatures.addAll(findComplementaryFeatures(seq, fr2));
+      }
 
+      /*
+       * sort features here if wanted
+       */
+      for (SequenceFeature sf : seqFeatures)
+      {
+        formatGffFeature(out, seq, sf);
         out.append(newline);
       }
     }
@@ -966,6 +1095,46 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
+   * Formats one feature as GFF and appends to the string buffer
+   */
+  private void formatGffFeature(StringBuilder out, SequenceI seq,
+          SequenceFeature sf)
+  {
+    String source = sf.featureGroup;
+    if (source == null)
+    {
+      source = sf.getDescription();
+    }
+
+    out.append(seq.getName());
+    out.append(TAB);
+    out.append(source);
+    out.append(TAB);
+    out.append(sf.type);
+    out.append(TAB);
+    out.append(sf.begin);
+    out.append(TAB);
+    out.append(sf.end);
+    out.append(TAB);
+    out.append(sf.score);
+    out.append(TAB);
+
+    int strand = sf.getStrand();
+    out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
+    out.append(TAB);
+
+    String phase = sf.getPhase();
+    out.append(phase == null ? "." : phase);
+
+    // miscellaneous key-values (GFF column 9)
+    String attributes = sf.getAttributes();
+    if (attributes != null)
+    {
+      out.append(TAB).append(attributes);
+    }
+  }
+
+  /**
    * Returns a mapping given list of one or more Align descriptors (exonerate
    * format)
    * 
index dd09d03..f2f0657 100644 (file)
@@ -24,6 +24,7 @@ import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.GeneLociI;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
@@ -63,7 +64,7 @@ public class SequenceAnnotationReport
    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
    * with 'Primary' sources placed before others, and 'chromosome' first of all
    */
-  private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
+  private static Comparator<DBRefEntry> comparator = new Comparator<>()
   {
 
     @Override
@@ -126,19 +127,33 @@ public class SequenceAnnotationReport
    * Append text for the list of features to the tooltip
    * 
    * @param sb
-   * @param rpos
+   * @param residuePos
    * @param features
    * @param minmax
    */
-  public void appendFeatures(final StringBuilder sb, int rpos,
+  public void appendFeatures(final StringBuilder sb, int residuePos,
           List<SequenceFeature> features, FeatureRendererModel fr)
   {
-    if (features != null)
+    for (SequenceFeature feature : features)
     {
-      for (SequenceFeature feature : features)
-      {
-        appendFeature(sb, rpos, fr, feature);
-      }
+      appendFeature(sb, residuePos, fr, feature, null);
+    }
+  }
+
+  /**
+   * Appends text for mapped features (e.g. CDS feature for peptide or vice versa)
+   * 
+   * @param sb
+   * @param residuePos
+   * @param mf
+   * @param fr
+   */
+  public void appendFeatures(StringBuilder sb, int residuePos,
+          MappedFeatures mf, FeatureRendererModel fr)
+  {
+    for (SequenceFeature feature : mf.features)
+    {
+      appendFeature(sb, residuePos, fr, feature, mf);
     }
   }
 
@@ -151,7 +166,8 @@ public class SequenceAnnotationReport
    * @param feature
    */
   void appendFeature(final StringBuilder sb, int rpos,
-          FeatureRendererModel fr, SequenceFeature feature)
+          FeatureRendererModel fr, SequenceFeature feature,
+          MappedFeatures mf)
   {
     if (feature.isContactFeature())
     {
@@ -220,6 +236,15 @@ public class SequenceAnnotationReport
           }
         }
       }
+
+      if (mf != null)
+      {
+        String variants = mf.findProteinVariants(feature);
+        if (!variants.isEmpty())
+        {
+          sb.append(" ").append(variants);
+        }
+      }
     }
   }
 
@@ -374,7 +399,7 @@ public class SequenceAnnotationReport
               .getNonPositionalFeatures())
       {
         int sz = -sb.length();
-        appendFeature(sb, 0, fr, sf);
+        appendFeature(sb, 0, fr, sf, null);
         sz += sb.length();
         maxWidth = Math.max(maxWidth, sz);
       }
index 7bf7791..d461811 100644 (file)
@@ -1,6 +1,5 @@
 package jalview.io.vcf;
 
-import jalview.analysis.AlignmentUtils;
 import jalview.analysis.Dna;
 import jalview.api.AlignViewControllerGuiI;
 import jalview.bin.Cache;
@@ -23,6 +22,8 @@ import jalview.util.MessageManager;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -51,6 +52,8 @@ import htsjdk.variant.vcf.VCFInfoHeaderLine;
  */
 public class VCFLoader
 {
+  private static final String UTF_8 = "UTF-8";
+
   private static final String DEFAULT_SPECIES = "homo_sapiens";
 
   /**
@@ -654,7 +657,8 @@ public class VCFLoader
         /*
          * dna-to-peptide product mapping
          */
-        AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
+        // JAL-3187 render on the fly instead
+        // AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
       }
       else
       {
@@ -1189,14 +1193,6 @@ public class VCFLoader
       }
 
       /*
-       * filter out fields we don't want to capture
-       */
-      if (!vcfFieldsOfInterest.contains(key))
-      {
-        continue;
-      }
-
-      /*
        * we extract values for other data which are allele-specific; 
        * these may be per alternate allele (INFO[key].Number = 'A') 
        * or per allele including reference (INFO[key].Number = 'R') 
@@ -1235,6 +1231,16 @@ public class VCFLoader
       String value = getAttributeValue(variant, key, index);
       if (value != null)
       {
+        /*
+         * VCF spec requires encoding of special characters e.g. '='
+         * so decode them here before storing
+         */
+        try
+        {
+          value = URLDecoder.decode(value, UTF_8);
+        } catch (UnsupportedEncodingException e)
+        {
+        }
         sf.setValue(key, value);
       }
     }
@@ -1288,6 +1294,16 @@ public class VCFLoader
             String id = vepFieldsOfInterest.get(i);
             if (id != null)
             {
+              /*
+               * VCF spec requires encoding of special characters e.g. '='
+               * so decode them here before storing
+               */
+              try
+              {
+                field = URLDecoder.decode(field, UTF_8);
+              } catch (UnsupportedEncodingException e)
+              {
+              }
               csqValues.put(id, field);
             }
           }
index 0e17779..5b1c0a3 100644 (file)
@@ -60,7 +60,6 @@ import jalview.gui.AlignmentPanel;
 import jalview.gui.AppVarna;
 import jalview.gui.ChimeraViewFrame;
 import jalview.gui.Desktop;
-import jalview.gui.FeatureRenderer;
 import jalview.gui.JvOptionPane;
 import jalview.gui.OOMWarning;
 import jalview.gui.PCAPanel;
@@ -94,6 +93,7 @@ import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.PCAModel;
 import jalview.viewmodel.ViewportRanges;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
 import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
 import jalview.ws.jws2.Jws2Discoverer;
@@ -1501,11 +1501,14 @@ public class Jalview2XML
       view.setFollowHighlight(av.isFollowHighlight());
       view.setFollowSelection(av.followSelection);
       view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus());
+      view.setShowComplementFeatures(av.isShowComplementFeatures());
+      view.setShowComplementFeaturesOnTop(
+              av.isShowComplementFeaturesOnTop());
       if (av.getFeaturesDisplayed() != null)
       {
         FeatureSettings fs = new FeatureSettings();
 
-        FeatureRenderer fr = ap.getSeqPanel().seqCanvas
+        FeatureRendererModel fr = ap.getSeqPanel().seqCanvas
                 .getFeatureRenderer();
         String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
 
@@ -5019,11 +5022,14 @@ public class Jalview2XML
     viewport.setShowNPFeats(safeBoolean(view.isShowNPfeatureTooltip()));
     viewport.setShowGroupConsensus(view.isShowGroupConsensus());
     viewport.setShowGroupConservation(view.isShowGroupConservation());
+    viewport.setShowComplementFeatures(view.isShowComplementFeatures());
+    viewport.setShowComplementFeaturesOnTop(
+            view.isShowComplementFeaturesOnTop());
 
     // recover feature settings
     if (jm.getFeatureSettings() != null)
     {
-      FeatureRenderer fr = af.alignPanel.getSeqPanel().seqCanvas
+      FeatureRendererModel fr = af.alignPanel.getSeqPanel().seqCanvas
               .getFeatureRenderer();
       FeaturesDisplayed fdi;
       viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
index cfe2735..d5784b0 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.renderer.seqfeatures;
 
+import jalview.api.AlignViewportI;
 import jalview.api.FeatureRenderer;
 import jalview.api.FeaturesDisplayedI;
 import jalview.datamodel.SequenceI;
@@ -122,8 +123,16 @@ public class FeatureColourFinder
    */
   boolean noFeaturesDisplayed()
   {
-    if (featureRenderer == null
-            || !featureRenderer.getViewport().isShowSequenceFeatures())
+    if (featureRenderer == null)
+    {
+      return true;
+    }
+    AlignViewportI av = featureRenderer.getViewport();
+    if (av.isShowComplementFeatures())
+    {
+      return false;
+    }
+    if (!av.isShowSequenceFeatures())
     {
       return true;
     }
index 13885b4..a1980c7 100644 (file)
@@ -23,9 +23,13 @@ package jalview.renderer.seqfeatures;
 import jalview.api.AlignViewportI;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.ContiguousI;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
 import jalview.util.Comparison;
+import jalview.util.ReverseListIterator;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.AlphaComposite;
@@ -273,7 +277,8 @@ public class FeatureRenderer extends FeatureRendererModel
      * if columns are all gapped, or sequence has no features, nothing to do
      */
     ContiguousI visiblePositions = seq.findPositions(start + 1, end + 1);
-    if (visiblePositions == null || !seq.getFeatures().hasFeatures())
+    if (visiblePositions == null || !seq.getFeatures().hasFeatures()
+            && !av.isShowComplementFeatures())
     {
       return null;
     }
@@ -290,6 +295,16 @@ public class FeatureRenderer extends FeatureRendererModel
     Color drawnColour = null;
 
     /*
+     * draw 'complement' features below ours if configured to do so
+     */
+    if (av.isShowComplementFeatures()
+            && !av.isShowComplementFeaturesOnTop())
+    {
+      drawnColour = drawComplementFeatures(g, seq, start, end, y1,
+              colourOnly, visiblePositions, drawnColour);
+    }
+
+    /*
      * iterate over features in ordering of their rendering (last is on top)
      */
     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
@@ -315,7 +330,7 @@ public class FeatureRenderer extends FeatureRendererModel
         if (featureColour == null)
         {
           /*
-           * feature excluded by visibility settings, filters, or colour threshold
+           * feature excluded by filters, or colour threshold
            */
           continue;
         }
@@ -394,6 +409,15 @@ public class FeatureRenderer extends FeatureRendererModel
       }
     }
 
+    /*
+     * draw 'complement' features above ours if configured to do so
+     */
+    if (av.isShowComplementFeatures() && av.isShowComplementFeaturesOnTop())
+    {
+      drawnColour = drawComplementFeatures(g, seq, start, end, y1,
+              colourOnly, visiblePositions, drawnColour);
+    }
+
     if (transparency != 1.0f && g != null)
     {
       /*
@@ -407,6 +431,52 @@ public class FeatureRenderer extends FeatureRendererModel
   }
 
   /**
+   * Find any features on the CDS/protein complement of the sequence region and
+   * draw them, with visibility and colouring as configured in the complementary
+   * viewport
+   * 
+   * @param g
+   * @param seq
+   * @param start
+   * @param end
+   * @param y1
+   * @param colourOnly
+   * @param visiblePositions
+   * @param drawnColour
+   * @return
+   */
+  Color drawComplementFeatures(final Graphics g, final SequenceI seq,
+          int start, int end, int y1, boolean colourOnly,
+          ContiguousI visiblePositions, Color drawnColour)
+  {
+    AlignViewportI comp = av.getCodingComplement();
+    FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp)
+            .getFeatureRenderer();
+
+    final int visibleStart = visiblePositions.getBegin();
+    final int visibleEnd = visiblePositions.getEnd();
+
+    for (int pos = visibleStart; pos <= visibleEnd; pos++)
+    {
+      int column = seq.findIndex(pos);
+      MappedFeatures mf = fr2
+              .findComplementFeaturesAtResidue(seq, pos);
+      if (mf != null)
+      {
+        for (SequenceFeature sf : mf.features)
+        {
+          FeatureColourI fc = fr2.getFeatureStyle(sf.getType());
+          Color featureColour = fr2.getColor(sf, fc);
+          renderFeature(g, seq, column - 1, column - 1, featureColour,
+                  start, end, y1, colourOnly);
+          drawnColour = featureColour;
+        }
+      }
+    }
+    return drawnColour;
+  }
+
+  /**
    * Called when alignment in associated view has new/modified features to
    * discover and display.
    * 
@@ -441,6 +511,18 @@ public class FeatureRenderer extends FeatureRendererModel
     updateFeatures();
 
     /*
+     * show complement features on top (if configured to show them)
+     */
+    if (av.isShowComplementFeatures() && av.isShowComplementFeaturesOnTop())
+    {
+      Color col = findComplementFeatureColour(seq, column);
+      if (col != null)
+      {
+        return col;
+      }
+    }
+
+    /*
      * inspect features in reverse renderOrder (the last in the array is 
      * displayed on top) until we find one that is rendered at the position
      */
@@ -469,8 +551,43 @@ public class FeatureRenderer extends FeatureRendererModel
     }
 
     /*
-     * no displayed feature found at position
+     * show complement features underneath (if configured to show them)
      */
+    Color col = null;
+    if (av.isShowComplementFeatures()
+            && !av.isShowComplementFeaturesOnTop())
+    {
+      col = findComplementFeatureColour(seq, column);
+    }
+
+    return col;
+  }
+
+  Color findComplementFeatureColour(SequenceI seq, int column)
+  {
+    AlignViewportI complement = av.getCodingComplement();
+    AlignFrame af = Desktop.getAlignFrameFor(complement);
+    FeatureRendererModel fr2 = af.getFeatureRenderer();
+    MappedFeatures mf = fr2.findComplementFeaturesAtResidue(
+            seq, seq.findPosition(column - 1));
+    if (mf == null)
+    {
+      return null;
+    }
+    ReverseListIterator<SequenceFeature> it = new ReverseListIterator<>(
+            mf.features);
+    while (it.hasNext())
+    {
+      SequenceFeature sf = it.next();
+      if (!fr2.featureGroupNotShown(sf))
+      {
+        Color col = fr2.getColour(sf);
+        if (col != null)
+        {
+          return col;
+        }
+      }
+    }
     return null;
   }
 }
index 81ff739..f8c5bea 100644 (file)
@@ -28,7 +28,15 @@ public interface SequenceListener
   // TODO remove this? never called on SequenceListener type
   public void mouseOverSequence(SequenceI sequence, int index, int pos);
 
-  public void highlightSequence(SearchResultsI results);
+  /**
+   * Highlights any position(s) represented by the search results and
+   * (optionally) returns an informative message about the position(s)
+   * higlighted
+   * 
+   * @param results
+   * @return
+   */
+  public String highlightSequence(SearchResultsI results);
 
   // TODO remove this? never called
   public void updateColours(SequenceI sequence, int index);
index ff88bb7..054ed2f 100644 (file)
@@ -859,13 +859,14 @@ public class StructureSelectionManager
    * @param pdbResNum
    * @param chain
    * @param pdbfile
+   * @return
    */
-  public void mouseOverStructure(int pdbResNum, String chain,
+  public String mouseOverStructure(int pdbResNum, String chain,
           String pdbfile)
   {
     AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0);
     List<AtomSpec> atoms = Collections.singletonList(atomSpec);
-    mouseOverStructure(atoms);
+    return mouseOverStructure(atoms);
   }
 
   /**
@@ -873,12 +874,12 @@ public class StructureSelectionManager
    * 
    * @param atoms
    */
-  public void mouseOverStructure(List<AtomSpec> atoms)
+  public String mouseOverStructure(List<AtomSpec> atoms)
   {
     if (listeners == null)
     {
       // old or prematurely sent event
-      return;
+      return null;
     }
     boolean hasSequenceListener = false;
     for (int i = 0; i < listeners.size(); i++)
@@ -890,18 +891,24 @@ public class StructureSelectionManager
     }
     if (!hasSequenceListener)
     {
-      return;
+      return null;
     }
 
     SearchResultsI results = findAlignmentPositionsForStructurePositions(
             atoms);
+    String result = null;
     for (Object li : listeners)
     {
       if (li instanceof SequenceListener)
       {
-        ((SequenceListener) li).highlightSequence(results);
+        String s = ((SequenceListener) li).highlightSequence(results);
+        if (s != null)
+        {
+          result = s;
+        }
       }
     }
+    return result;
   }
 
   /**
index 9f28ee1..731e976 100644 (file)
@@ -1236,4 +1236,14 @@ public class MapList
     return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
   }
 
+  /**
+   * Answers true if the mapping is from one contiguous range to another, else
+   * false
+   * 
+   * @return
+   */
+  public boolean isContiguous()
+  {
+    return fromShifts.size() == 1 && toShifts.size() == 1;
+  }
 }
index 148ea16..8dcd1b3 100644 (file)
@@ -2716,6 +2716,30 @@ public abstract class AlignmentViewport
     viewStyle.setProteinFontAsCdna(b);
   }
 
+  @Override
+  public void setShowComplementFeatures(boolean b)
+  {
+    viewStyle.setShowComplementFeatures(b);
+  }
+
+  @Override
+  public boolean isShowComplementFeatures()
+  {
+    return viewStyle.isShowComplementFeatures();
+  }
+
+  @Override
+  public void setShowComplementFeaturesOnTop(boolean b)
+  {
+    viewStyle.setShowComplementFeaturesOnTop(b);
+  }
+
+  @Override
+  public boolean isShowComplementFeaturesOnTop()
+  {
+    return viewStyle.isShowComplementFeaturesOnTop();
+  }
+
   /**
    * @return true if view should scroll to show the highlighted region of a
    *         sequence
index 17f9362..f9c9782 100644 (file)
@@ -23,7 +23,13 @@ package jalview.viewmodel.seqfeatures;
 import jalview.api.AlignViewportI;
 import jalview.api.FeatureColourI;
 import jalview.api.FeaturesDisplayedI;
+import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.MappedFeatures;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.SearchResultMatchI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcherSetI;
@@ -320,12 +326,12 @@ public abstract class FeatureRendererModel
             visibleTypes);
 
     /*
-     * include features unless their feature group is not displayed, or
-     * they are hidden (have no colour) based on a filter or colour threshold
+     * include features unless they are hidden (have no colour), based on 
+     * feature group visibility, or a filter or colour threshold
      */
     for (SequenceFeature sf : features)
     {
-      if (!featureGroupNotShown(sf) && getColour(sf) != null)
+      if (getColour(sf) != null)
       {
         result.add(sf);
       }
@@ -983,7 +989,7 @@ public abstract class FeatureRendererModel
    * @param sequenceFeature
    * @return
    */
-  protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature)
+  public boolean featureGroupNotShown(final SequenceFeature sequenceFeature)
   {
     return featureGroups != null
             && sequenceFeature.featureGroup != null
@@ -998,7 +1004,7 @@ public abstract class FeatureRendererModel
    */
   @Override
   public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
-          int resNo)
+          int fromResNo, int toResNo)
   {
     List<SequenceFeature> result = new ArrayList<>();
     if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
@@ -1011,12 +1017,11 @@ public abstract class FeatureRendererModel
      * displayed, and feature group is null or the empty string
      * or marked for display
      */
-    Set<String> visibleFeatures = getFeaturesDisplayed()
-            .getVisibleFeatures();
+    List<String> visibleFeatures = getDisplayedFeatureTypes();
     String[] visibleTypes = visibleFeatures
             .toArray(new String[visibleFeatures.size()]);
     List<SequenceFeature> features = sequence.getFeatures().findFeatures(
-            resNo, resNo, visibleTypes);
+            fromResNo, toResNo, visibleTypes);
   
     for (SequenceFeature sf : features)
     {
@@ -1157,6 +1162,89 @@ public abstract class FeatureRendererModel
   }
 
   @Override
+  public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence,
+          int pos)
+  {
+    SequenceI ds = sequence.getDatasetSequence();
+    if (ds == null)
+    {
+      ds = sequence;
+    }
+    final char residue = ds.getCharAt(pos - ds.getStart());
+
+    List<SequenceFeature> found = new ArrayList<>();
+    List<AlignedCodonFrame> mappings = this.av.getAlignment()
+            .getCodonFrame(sequence);
+
+    /*
+     * todo: direct lookup of CDS for peptide and vice-versa; for now,
+     * have to search through an unordered list of mappings for a candidate
+     */
+    Mapping mapping = null;
+    SequenceI mapFrom = null;
+
+    for (AlignedCodonFrame acf : mappings)
+    {
+      mapping = acf.getMappingForSequence(sequence);
+      if (mapping == null || !mapping.getMap().isTripletMap())
+      {
+        continue; // we are only looking for 3:1 or 1:3 mappings
+      }
+      SearchResultsI sr = new SearchResults();
+      acf.markMappedRegion(ds, pos, sr);
+      for (SearchResultMatchI match : sr.getResults())
+      {
+        int fromRes = match.getStart();
+        int toRes = match.getEnd();
+        mapFrom = match.getSequence();
+        List<SequenceFeature> fs = findFeaturesAtResidue(
+                match.getSequence(), fromRes, toRes);
+        for (SequenceFeature sf : fs)
+        {
+          if (!found.contains(sf))
+          {
+            found.add(sf);
+          }
+        }
+      }
+
+      /*
+       * just take the first mapped features we find
+       */
+      if (!found.isEmpty())
+      {
+        break;
+      }
+    }
+    if (found.isEmpty())
+    {
+      return null;
+    }
+
+    /*
+     * sort by renderorder, inefficiently
+     */
+    List<SequenceFeature> result = new ArrayList<>();
+    for (String type : renderOrder)
+    {
+      for (SequenceFeature sf : found)
+      {
+        if (type.equals(sf.getType()))
+        {
+          result.add(sf);
+          if (result.size() == found.size())
+          {
+            return new MappedFeatures(mapping, mapFrom, pos, residue,
+                    result);
+          }
+        }
+      }
+    }
+    
+    return new MappedFeatures(mapping, mapFrom, pos, residue, result);
+  }
+
+  @Override
   public boolean isVisible(SequenceFeature feature)
   {
     if (feature == null)
index 16aa580..91f2f0c 100644 (file)
@@ -213,6 +213,8 @@ public class ViewStyle implements ViewStyleI
     setShowNPFeats(vs.isShowNPFeats());
     setShowSequenceFeaturesHeight(vs.isShowSequenceFeaturesHeight());
     setShowSequenceFeatures(vs.isShowSequenceFeatures());
+    setShowComplementFeatures(vs.isShowComplementFeatures());
+    setShowComplementFeaturesOnTop(vs.isShowComplementFeaturesOnTop());
     setShowText(vs.getShowText());
     setShowUnconserved(vs.getShowUnconserved());
     setTextColour(vs.getTextColour());
@@ -275,6 +277,9 @@ public class ViewStyle implements ViewStyleI
             && isShowSequenceFeaturesHeight() == vs
                     .isShowSequenceFeaturesHeight()
             && isShowSequenceFeatures() == vs.isShowSequenceFeatures()
+            && isShowComplementFeatures() == vs.isShowComplementFeatures()
+            && isShowComplementFeaturesOnTop() == vs
+                    .isShowComplementFeaturesOnTop()
             && getShowText() == vs.getShowText()
             && getShowUnconserved() == vs.getShowUnconserved()
             && getThreshold() == vs.getThreshold()
@@ -365,6 +370,10 @@ public class ViewStyle implements ViewStyleI
 
   private int fontStyle;
 
+  private boolean showComplementFeatures;
+
+  private boolean showComplementFeaturesOnTop;
+
   /**
    * GUI state
    * 
@@ -1111,4 +1120,28 @@ public class ViewStyle implements ViewStyleI
   {
     proteinFontAsCdna = b;
   }
+
+  @Override
+  public void setShowComplementFeatures(boolean b)
+  {
+    showComplementFeatures = b;
+  }
+
+  @Override
+  public boolean isShowComplementFeatures()
+  {
+    return showComplementFeatures;
+  }
+
+  @Override
+  public void setShowComplementFeaturesOnTop(boolean b)
+  {
+    showComplementFeaturesOnTop = b;
+  }
+
+  @Override
+  public boolean isShowComplementFeaturesOnTop()
+  {
+    return showComplementFeaturesOnTop;
+  }
 }
index 14559dd..dabd3ee 100644 (file)
@@ -2044,41 +2044,48 @@ public class AlignmentUtilsTests
      * NB setting "id" (as returned by Ensembl for features in JSON format);
      * previously "ID" (as returned for GFF3 format)
      */
-    SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1,
+    SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 10,
+            10,
             0f, ensembl);
     sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E
     sf1.setValue("id", "var1.125A>G");
 
-    SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1,
+    SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 10,
+            10,
             0f, dbSnp);
     sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q
     sf2.setValue("id", "var2");
     sf2.setValue("clinical_significance", "Dodgy");
 
-    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1,
+    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 11,
+            11,
             0f, dbSnp);
     sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon
     sf3.setValue("id", "var3");
     sf3.setValue("clinical_significance", "Bad");
 
-    SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3,
+    SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 12,
+            12,
             0f, cosmic);
     sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous
     sf4.setValue("id", "var4");
     sf4.setValue("clinical_significance", "None");
 
-    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3,
+    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 12,
+            12,
             0f, ensembl);
     sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N
     sf5.setValue("id", "sequence_variant:var5"); // prefix gets stripped off
     sf5.setValue("clinical_significance", "Benign");
 
-    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6,
+    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 15,
+            15,
             0f, dbSnp);
     sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous
     sf6.setValue("id", "var6");
 
-    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8,
+    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 17,
+            17,
             0f, cosmic);
     sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R
     sf7.setValue("id", "var7");
@@ -2103,7 +2110,7 @@ public class AlignmentUtilsTests
     // codon2Variants.add(new DnaVariant("A"));
     codon3Variants.add(new DnaVariant("A", sf4));
     codon3Variants.add(new DnaVariant("A", sf5));
-    AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants);
+    AlignmentUtils.computePeptideVariants(peptide, 10, codonVariants);
 
     /*
      * compute variants for protein position 2
@@ -2114,7 +2121,7 @@ public class AlignmentUtilsTests
     codon1Variants.add(new DnaVariant("T"));
     codon2Variants.add(new DnaVariant("T"));
     codon3Variants.add(new DnaVariant("T", sf6));
-    AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants);
+    AlignmentUtils.computePeptideVariants(peptide, 11, codonVariants);
 
     /*
      * compute variants for protein position 3
@@ -2125,7 +2132,7 @@ public class AlignmentUtilsTests
     codon1Variants.add(new DnaVariant("C"));
     codon2Variants.add(new DnaVariant("C", sf7));
     codon3Variants.add(new DnaVariant("C"));
-    AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants);
+    AlignmentUtils.computePeptideVariants(peptide, 12, codonVariants);
 
     /*
      * verify added sequence features for
@@ -2149,55 +2156,55 @@ public class AlignmentUtilsTests
      */
     // AAA -> AAT -> K/N
     SequenceFeature sf = sfs.get(0);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
+    assertEquals(10, sf.getBegin());
+    assertEquals(10, sf.getEnd());
     assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Asn", sf.getDescription());
+    assertEquals("p.Lys10Asn", sf.getDescription());
     assertEquals("var5", sf.getValue("id"));
     assertEquals("Benign", sf.getValue("clinical_significance"));
     assertEquals("id=var5;clinical_significance=Benign",
             sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5",
+            "p.Lys10Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
     // AAA -> CAA -> K/Q
     sf = sfs.get(1);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
+    assertEquals(10, sf.getBegin());
+    assertEquals(10, sf.getEnd());
     assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Gln", sf.getDescription());
+    assertEquals("p.Lys10Gln", sf.getDescription());
     assertEquals("var2", sf.getValue("id"));
     assertEquals("Dodgy", sf.getValue("clinical_significance"));
     assertEquals("id=var2;clinical_significance=Dodgy", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Lys1Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2",
+            "p.Lys10Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2",
             sf.links.get(0));
     assertEquals(dbSnp, sf.getFeatureGroup());
 
     // AAA -> GAA -> K/E
     sf = sfs.get(2);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
+    assertEquals(10, sf.getBegin());
+    assertEquals(10, sf.getEnd());
     assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Glu", sf.getDescription());
+    assertEquals("p.Lys10Glu", sf.getDescription());
     assertEquals("var1.125A>G", sf.getValue("id"));
     assertNull(sf.getValue("clinical_significance"));
     assertEquals("id=var1.125A>G", sf.getAttributes());
     assertEquals(1, sf.links.size());
     // link to variation is urlencoded
     assertEquals(
-            "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG",
+            "p.Lys10Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
     // AAA -> TAA -> stop codon
     sf = sfs.get(3);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
+    assertEquals(10, sf.getBegin());
+    assertEquals(10, sf.getEnd());
     assertEquals("stop_gained", sf.getType());
     assertEquals("Aaa/Taa", sf.getDescription());
     assertEquals("var3", sf.getValue("id"));
@@ -2211,8 +2218,8 @@ public class AlignmentUtilsTests
 
     // AAA -> AAG synonymous
     sf = sfs.get(4);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
+    assertEquals(10, sf.getBegin());
+    assertEquals(10, sf.getEnd());
     assertEquals("synonymous_variant", sf.getType());
     assertEquals("aaA/aaG", sf.getDescription());
     assertEquals("var4", sf.getValue("id"));
@@ -2226,8 +2233,8 @@ public class AlignmentUtilsTests
 
     // TTT -> TTC synonymous
     sf = sfs.get(5);
-    assertEquals(2, sf.getBegin());
-    assertEquals(2, sf.getEnd());
+    assertEquals(11, sf.getBegin());
+    assertEquals(11, sf.getEnd());
     assertEquals("synonymous_variant", sf.getType());
     assertEquals("ttT/ttC", sf.getDescription());
     assertEquals("var6", sf.getValue("id"));
@@ -2242,31 +2249,31 @@ public class AlignmentUtilsTests
     // var7 generates two distinct protein variant features (two alleles)
     // CCC -> CGC -> P/R
     sf = sfs.get(6);
-    assertEquals(3, sf.getBegin());
-    assertEquals(3, sf.getEnd());
+    assertEquals(12, sf.getBegin());
+    assertEquals(12, sf.getEnd());
     assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Pro3Arg", sf.getDescription());
+    assertEquals("p.Pro12Arg", sf.getDescription());
     assertEquals("var7", sf.getValue("id"));
     assertEquals("Good", sf.getValue("clinical_significance"));
     assertEquals("id=var7;clinical_significance=Good", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
+            "p.Pro12Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
 
     // CCC -> CAC -> P/H
     sf = sfs.get(7);
-    assertEquals(3, sf.getBegin());
-    assertEquals(3, sf.getEnd());
+    assertEquals(12, sf.getBegin());
+    assertEquals(12, sf.getEnd());
     assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Pro3His", sf.getDescription());
+    assertEquals("p.Pro12His", sf.getDescription());
     assertEquals("var7", sf.getValue("id"));
     assertEquals("Good", sf.getValue("clinical_significance"));
     assertEquals("id=var7;clinical_significance=Good", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
+            "p.Pro12His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
   }
@@ -2591,9 +2598,16 @@ public class AlignmentUtilsTests
     AlignmentI al2 = new Alignment(new SequenceI[] { dna3, dna4 });
     ((Alignment) al2).createDatasetAlignment();
 
+    /*
+     * alignment removes gapped columns (two internal, two trailing)
+     */
     assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2));
-    assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString());
-    assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString());
+    String aligned1 = "-cc-GG-GTTT-aaa";
+    assertEquals(aligned1,
+            al1.getSequenceAt(0).getSequenceAsString());
+    String aligned2 = "C--C-Cgg-gtttAAA";
+    assertEquals(aligned2,
+            al1.getSequenceAt(1).getSequenceAsString());
 
     /*
      * add another sequence to 'aligned' - should still succeed, since
@@ -2603,8 +2617,8 @@ public class AlignmentUtilsTests
     dna5.createDatasetSequence();
     al2.addSequence(dna5);
     assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2));
-    assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString());
-    assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString());
+    assertEquals(aligned1, al1.getSequenceAt(0).getSequenceAsString());
+    assertEquals(aligned2, al1.getSequenceAt(1).getSequenceAsString());
 
     /*
      * add another sequence to 'unaligned' - should fail, since now not
@@ -2622,15 +2636,15 @@ public class AlignmentUtilsTests
   {
     SequenceI dna1 = new Sequence("dna1", "cccGGGTTTaaa");
     SequenceI dna2 = new Sequence("dna2", "CCCgggtttAAA");
-    SequenceI as1 = dna1.deriveSequence();
-    SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7);
-    SequenceI as3 = dna2.deriveSequence();
+    SequenceI as1 = dna1.deriveSequence(); // cccGGGTTTaaa/1-12
+    SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7); // GGGT/4-7
+    SequenceI as3 = dna2.deriveSequence(); // CCCgggtttAAA/1-12
     as1.insertCharAt(6, 5, '-');
-    String s_as1 = as1.getSequenceAsString();
+    assertEquals("cccGGG-----TTTaaa", as1.getSequenceAsString());
     as2.insertCharAt(6, 5, '-');
-    String s_as2 = as2.getSequenceAsString();
-    as3.insertCharAt(6, 5, '-');
-    String s_as3 = as3.getSequenceAsString();
+    assertEquals("GGGT-----", as2.getSequenceAsString());
+    as3.insertCharAt(3, 5, '-');
+    assertEquals("CCC-----gggtttAAA", as3.getSequenceAsString());
     AlignmentI aligned = new Alignment(new SequenceI[] { as1, as2, as3 });
 
     // why do we need to cast this still ?
@@ -2642,10 +2656,13 @@ public class AlignmentUtilsTests
         uas3 });
     ((Alignment) tobealigned).createDatasetAlignment();
 
+    /*
+     * alignAs lines up dataset sequences and removes empty columns (two)
+     */
     assertTrue(AlignmentUtils.alignAsSameSequences(tobealigned, aligned));
-    assertEquals(s_as1, uas1.getSequenceAsString());
-    assertEquals(s_as2, uas2.getSequenceAsString());
-    assertEquals(s_as3, uas3.getSequenceAsString());
+    assertEquals("cccGGG---TTTaaa", uas1.getSequenceAsString());
+    assertEquals("GGGT", uas2.getSequenceAsString());
+    assertEquals("CCC---gggtttAAA", uas3.getSequenceAsString());
   }
 
   @Test(groups = { "Functional" })
diff --git a/test/jalview/datamodel/MappedFeaturesTest.java b/test/jalview/datamodel/MappedFeaturesTest.java
new file mode 100644 (file)
index 0000000..e4caac3
--- /dev/null
@@ -0,0 +1,113 @@
+package jalview.datamodel;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.util.MapList;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class MappedFeaturesTest
+{
+  @Test
+  public void testFindProteinVariants()
+  {
+    /*
+     * scenario: 
+     * dna/10-20 aCGTaGctGAa (codons CGT=R, GGA = G)
+     * mapping: 3:1 from [11-13,15,18-19] to peptide/1-2 RG 
+     */
+    SequenceI from = new Sequence("dna/10-20", "ACGTAGCTGAA");
+    SequenceI to = new Sequence("peptide", "RG");
+    MapList map = new MapList(new int[] { 11, 13, 15, 15, 18, 19 },
+            new int[]
+            { 1, 2 }, 3, 1);
+    Mapping mapping = new Mapping(to, map);
+
+    /*
+     * variants
+     * C>T at dna11, consequence CGT>TGT=C
+     * T>C at dna13, consequence CGT>CGC synonymous
+     */
+    List<SequenceFeature> features = new ArrayList<>();
+    SequenceFeature sf1 = new SequenceFeature("sequence_variant", "C,T",
+            11, 11, null);
+    sf1.setValue("alleles", "C,T");
+    features.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("sequence_variant", "T,C", 13,
+            13, null);
+    sf2.setValue("alleles", "T,C");
+    features.add(sf2);
+
+    /*
+     * missense variant in first codon
+     */
+    MappedFeatures mf = new MappedFeatures(mapping, from, 1, 'R',
+            features);
+    String variant = mf.findProteinVariants(sf1);
+    assertEquals(variant, "p.Arg1Cys");
+
+    /*
+     * more than one alternative allele
+     * C>G consequence is GGT=G
+     * peptide variants as a comma-separated list
+     */
+    sf1.setValue("alleles", "C,T,G");
+    variant = mf.findProteinVariants(sf1);
+    assertEquals(variant, "p.Arg1Cys,p.Arg1Gly");
+
+    /*
+     * synonymous variant in first codon
+     * shown in HGVS notation on peptide
+     */
+    variant = mf.findProteinVariants(sf2);
+    assertEquals(variant, "c.13T>C(p.=)");
+
+    /*
+     * CSQ:HGVSp value is used if present
+     */
+    Map<String, String> csq = new HashMap<>();
+    csq.put("HGVSp", "hello:world");
+    sf2.setValue("CSQ", csq);
+    variant = mf.findProteinVariants(sf2);
+    assertEquals(variant, "world");
+
+    /*
+     * missense and indel variants in second codon
+     * - codon is GGA spliced from dna positions 15,18,19
+     * - SNP G>T in second position mutates GGA>G to GTA>V
+     * - indel variants are not computed or reported
+     */
+    mf = new MappedFeatures(mapping, from, 2, 'G', features);
+    features.clear();
+    SequenceFeature sf3 = new SequenceFeature("sequence_variant",
+            "G,-,CG,T", 18, 18, null);
+    sf3.setValue("alleles", "G,-,CG,T");
+    features.add(sf3);
+    variant = mf.findProteinVariants(sf3);
+    assertEquals(variant, "p.Gly2Val");
+
+    /*
+     * G>T in first position gives TGA Stop
+     * shown with HGVS notation as 'Ter'
+     */
+    SequenceFeature sf4 = new SequenceFeature("sequence_variant", "G,T", 15,
+            15, null);
+    sf4.setValue("alleles", "G,-,CG,T");
+    features.add(sf4);
+    variant = mf.findProteinVariants(sf4);
+    assertEquals(variant, "p.Gly2Ter");
+
+    /*
+     * feature must be one of those in MappedFeatures
+     */
+    SequenceFeature sf9 = new SequenceFeature("sequence_variant", "G,C", 15,
+            15, null);
+    variant = mf.findProteinVariants(sf9);
+    assertEquals(variant, "");
+  }
+}
index 9111e5a..cd8f9eb 100644 (file)
@@ -277,43 +277,46 @@ public class SequenceFeatureTest
   @Test(groups = { "Functional" })
   public void testGetDetailsReport()
   {
+    SequenceI seq = new Sequence("TestSeq", "PLRFQMD");
+    String seqName = seq.getName();
+
     // single locus, no group, no score
     SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null);
-    String expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>22</td><td></td></tr>"
+    String expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>22</td></tr>"
+            + "<tr><td>Type</td><td>variant</td><td></td></tr>"
             + "<tr><td>Description</td><td>G,C</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
 
     // contact feature
     sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31,
             null);
-    expected = "<br><table><tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>28:31</td><td></td></tr>"
+    expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>28:31</td></tr>"
+            + "<tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
             + "<tr><td>Description</td><td>a description</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
 
     sf = new SequenceFeature("variant", "G,C", 22, 33,
             12.5f, "group");
     sf.setValue("Parent", "ENSG001");
     sf.setValue("Child", "ENSP002");
-    expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>22-33</td><td></td></tr>"
+    expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>22-33</td></tr>"
+            + "<tr><td>Type</td><td>variant</td><td></td></tr>"
             + "<tr><td>Description</td><td>G,C</td><td></td></tr>"
             + "<tr><td>Score</td><td>12.5</td><td></td></tr>"
             + "<tr><td>Group</td><td>group</td><td></td></tr>"
             + "<tr><td>Child</td><td></td><td>ENSP002</td></tr>"
             + "<tr><td>Parent</td><td></td><td>ENSG001</td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
 
     /*
      * feature with embedded html link in description
      */
     String desc = "<html>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></html>";
     sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot");
-    expected = "<br><table><tr><td>Type</td><td>Pfam</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>8-83</td><td></td></tr>"
+    expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>8-83</td></tr>"
+            + "<tr><td>Type</td><td>Pfam</td><td></td></tr>"
             + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td><td></td></tr>"
             + "<tr><td>Group</td><td>Uniprot</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
   }
 }
index 29e76bb..4198a37 100644 (file)
@@ -940,14 +940,14 @@ public class SequenceFeaturesTest
     assertFalse(iterator.hasNext());
 
     /*
-     * two types specified - get sorted alphabetically
+     * two types specified - order is preserved
      */
     types = sf.varargToTypes("Metal", "Cath");
     iterator = types.iterator();
     assertTrue(iterator.hasNext());
-    assertSame(iterator.next(), featureStores.get("Cath"));
-    assertTrue(iterator.hasNext());
     assertSame(iterator.next(), featureStores.get("Metal"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
     assertFalse(iterator.hasNext());
 
     /*
index 2c973ca..06a09df 100644 (file)
@@ -60,15 +60,15 @@ public class ChimeraCommandsTest
   {
 
     Map<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B");
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B");
-    ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A");
-    ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.red, 0, 6, 9, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A");
 
     // Colours should appear in the Chimera command in the order in which
     // they were added; within colour, by model, by chain, ranges in start order
@@ -91,7 +91,7 @@ public class ChimeraCommandsTest
      * start with just one feature/value...
      */
     featuresMap.put("chain", featureValues);
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 8, 20, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A");
   
     List<String> commands = ChimeraCommands
             .buildSetAttributeCommands(featuresMap);
@@ -104,24 +104,24 @@ public class ChimeraCommandsTest
     assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A");
 
     // add same feature value, overlapping range
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 3, 9, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A");
     // same feature value, contiguous range
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
     assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A");
 
     // same feature value and model, different chain
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "B");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B");
     // same feature value and chain, different model
-    ChimeraCommands.addColourRange(featureValues, "X", 1, 26, 30, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
     assertEquals(commands.get(0),
             "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A");
 
     // same feature, different value
-    ChimeraCommands.addColourRange(featureValues, "Y", 0, 40, 50, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(2, commands.size());
     // commands are ordered by feature type but not by value
@@ -133,7 +133,7 @@ public class ChimeraCommandsTest
     featuresMap.clear();
     featureValues.clear();
     featuresMap.put("side-chain binding!", featureValues);
-    ChimeraCommands.addColourRange(featureValues,
+    ChimeraCommands.addAtomSpecRange(featureValues,
             "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
             "A");
     // feature names are sanitised to change non-alphanumeric to underscore
index 3e82547..d2284f1 100644 (file)
@@ -261,8 +261,11 @@ public class AlignFrameTest
 
     /*
      * apply 30% Conservation to group
+     * (notice menu action applies to selection group even if mouse click
+     * is at a sequence not in the group)
      */
-    PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null);
+    PopupMenu popupMenu = new PopupMenu(af.alignPanel, al.getSequenceAt(2),
+            null);
     popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand
             .toString());
     assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
@@ -540,7 +543,8 @@ public class AlignFrameTest
     sg.setStartRes(15);
     sg.setEndRes(25);
     av.setSelectionGroup(sg);
-    PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null);
+    PopupMenu popupMenu = new PopupMenu(af.alignPanel, al.getSequenceAt(0),
+            null);
     popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand
             .toString());
     assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
index 6d8a47e..fd4bd4b 100644 (file)
@@ -15,6 +15,7 @@ import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
 import jalview.schemes.FeatureColourTest;
 import jalview.util.matcher.Condition;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.Color;
 import java.io.File;
@@ -50,7 +51,7 @@ public class FeatureSettingsTest
     /*
      * set colour schemes for features
      */
-    FeatureRenderer fr = af.getFeatureRenderer();
+    FeatureRendererModel fr = af.getFeatureRenderer();
 
     // type1: red
     fr.setColour("type1", new FeatureColour(Color.red));
index df30935..bf961d8 100644 (file)
@@ -91,6 +91,8 @@ public class PopupMenuTest
   public void setUp() throws IOException
   {
     Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Cache.initLogger();
+
     String inMenuString = ("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$"
             + SEQUENCE_ID
             + "$"
@@ -113,7 +115,7 @@ public class PopupMenuTest
             DataSourceType.PASTE, FileFormat.Fasta);
     AlignFrame af = new AlignFrame(alignment, 700, 500);
     parentPanel = new AlignmentPanel(af, af.getViewport());
-    testee = new PopupMenu(parentPanel, null, null);
+    testee = new PopupMenu(parentPanel, alignment.getSequenceAt(0), null);
     int i = 0;
     for (SequenceI seq : alignment.getSequences())
     {
@@ -534,7 +536,6 @@ public class PopupMenuTest
      * note dbref GENE3D is matched to link Gene3D, the latter is displayed
      */
     linkMenu = PopupMenu.buildLinkMenu(seq1, noFeatures);
-    assertEquals(linkText, linkMenu.getText());
     linkItems = linkMenu.getMenuComponents();
     assertEquals(3, linkItems.length);
     assertEquals("EMBL-EBI Search", ((JMenuItem) linkItems[0]).getText());
@@ -553,10 +554,31 @@ public class PopupMenuTest
     Preferences.sequenceUrlLinks = factory.createUrlProvider();
 
     linkMenu = PopupMenu.buildLinkMenu(seq1, noFeatures);
-    assertEquals(linkText, linkMenu.getText());
     linkItems = linkMenu.getMenuComponents();
     assertEquals(1, linkItems.length);
     assertEquals("EMBL-EBI Search", ((JMenuItem) linkItems[0]).getText());
+
+    /*
+     * if sequence is null, only feature links are shown (alignment popup submenu)
+     */
+    linkMenu = PopupMenu.buildLinkMenu(null, noFeatures);
+    linkItems = linkMenu.getMenuComponents();
+    assertEquals(0, linkItems.length);
+
+    List<SequenceFeature> features = new ArrayList<>();
+    SequenceFeature sf = new SequenceFeature("type", "desc", 1, 20, null);
+    features.add(sf);
+    linkMenu = PopupMenu.buildLinkMenu(null, features);
+    linkItems = linkMenu.getMenuComponents();
+    assertEquals(0, linkItems.length); // feature has no links
+
+    sf.addLink("Pfam family|http://pfam.xfam.org/family/PF00111");
+    linkMenu = PopupMenu.buildLinkMenu(null, features);
+    linkItems = linkMenu.getMenuComponents();
+    assertEquals(1, linkItems.length);
+    JMenuItem item = (JMenuItem) linkItems[0];
+    assertEquals("Pfam family", item.getText());
+    // ? no way to verify URL, compiled into link's actionListener
   }
 
   @Test(groups = { "Functional" })
index 77c18db..090de6f 100644 (file)
@@ -476,7 +476,7 @@ public class FeaturesFileTest
      */
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     String exported = featuresFile
-            .printJalviewFormat(al.getSequencesArray(), fr, false);
+            .printJalviewFormat(al.getSequencesArray(), fr, false, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
@@ -485,7 +485,7 @@ public class FeaturesFileTest
      */
     fr.setGroupVisibility("uniprot", true);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            true);
+            true, false);
     expected = "\nSTARTGROUP\tuniprot\n"
             + "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n"
             + "ENDGROUP\tuniprot\n\n"
@@ -499,12 +499,12 @@ public class FeaturesFileTest
     fr.setVisible("METAL");
     fr.setVisible("GAMMA-TURN");
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "METAL\tcc9900\n"
             + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
-            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
+            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "ENDGROUP\tuniprot\n";
     assertEquals(expected, exported);
 
@@ -513,7 +513,7 @@ public class FeaturesFileTest
      */
     fr.setVisible("Pfam");
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     /*
      * features are output within group, ordered by sequence and type
      */
@@ -521,9 +521,9 @@ public class FeaturesFileTest
             + "Pfam\tff0000\n"
             + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
-            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "<html>Pfam domain<a href=\"http://pfam.xfam.org/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n"
+            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "ENDGROUP\tuniprot\n"
             // null / empty group features are output after named groups
             + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n"
@@ -539,14 +539,14 @@ public class FeaturesFileTest
             + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n"
             + "\ndesc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n";
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     assertEquals(expected, exported);
 
     /*
      * include non-positional (overrides group not shown)
      */
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            true);
+            true, false);
     expected = "METAL\tcc9900\n" + "Pfam\tff0000\n"
             + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
@@ -573,11 +573,11 @@ public class FeaturesFileTest
     FeatureRendererModel fr = (FeatureRendererModel) af.alignPanel
             .getFeatureRenderer();
     String exported = featuresFile.printGffFormat(al.getSequencesArray(),
-            fr, false);
+            fr, false, false);
     String gffHeader = "##gff-version 2\n";
     assertEquals(gffHeader, exported);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            true);
+            true, false);
     assertEquals(gffHeader, exported);
 
     /*
@@ -614,7 +614,7 @@ public class FeaturesFileTest
      * with no features displayed, exclude non-positional features
      */
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     assertEquals(gffHeader, exported);
 
     /*
@@ -623,7 +623,7 @@ public class FeaturesFileTest
     fr.setGroupVisibility("Uniprot", true);
     fr.setGroupVisibility("s3dm", false);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            true);
+            true, false);
     String expected = gffHeader
             + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n";
     assertEquals(expected, exported);
@@ -635,7 +635,7 @@ public class FeaturesFileTest
     fr.setVisible("METAL");
     fr.setVisible("GAMMA-TURN");
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     // METAL feature has null group: description used for column 2
     expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n";
     assertEquals(expected, exported);
@@ -645,7 +645,7 @@ public class FeaturesFileTest
      */
     fr.setGroupVisibility("s3dm", true);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     // METAL feature has null group: description used for column 2
     expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
             + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n";
@@ -656,12 +656,12 @@ public class FeaturesFileTest
      */
     fr.setVisible("Pfam");
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     // Pfam feature columns include strand(+), phase(2), attributes
     expected = gffHeader
             + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
-            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"
-            + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n";
+            + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n"
+            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n";
     assertEquals(expected, exported);
   }
 
@@ -770,7 +770,7 @@ public class FeaturesFileTest
     fr.setVisible("METAL");
     fr.setColour("METAL", new FeatureColour(Color.PINK));
     String exported = featuresFile.printGffFormat(al.getSequencesArray(),
-            fr, false);
+            fr, false, false);
     String expected = gffHeader
             + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
             + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
@@ -785,7 +785,7 @@ public class FeaturesFileTest
     fc.setThreshold(1.1f);
     fr.setColour("METAL", fc);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n";
     assertEquals(expected, exported);
 
@@ -794,7 +794,7 @@ public class FeaturesFileTest
      */
     fc.setAboveThreshold(false);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
             + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
     assertEquals(expected, exported);
@@ -807,7 +807,7 @@ public class FeaturesFileTest
             "clin_sig"));
     fr.setFeatureFilter("METAL", filter);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
     assertEquals(expected, exported);
   }
@@ -843,7 +843,7 @@ public class FeaturesFileTest
     fr.setColour("METAL", new FeatureColour(Color.PINK));
     String exported = featuresFile.printJalviewFormat(
             al.getSequencesArray(),
-            fr, false);
+            fr, false, false);
     String expected = "METAL\tffafaf\n\nSTARTGROUP\tgrp1\n"
             + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n"
             + "ENDGROUP\tgrp1\n\nSTARTGROUP\tgrp2\n"
@@ -861,7 +861,7 @@ public class FeaturesFileTest
     fc.setThreshold(1.1f);
     fr.setColour("METAL", fc);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|above|1.1\n\n"
             + "STARTGROUP\tgrp1\n"
             + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n"
@@ -873,7 +873,7 @@ public class FeaturesFileTest
      */
     fc.setAboveThreshold(false);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n"
             + "STARTGROUP\tgrp1\n"
             + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n"
@@ -890,7 +890,7 @@ public class FeaturesFileTest
             "clin_sig"));
     fr.setFeatureFilter("METAL", filter);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
     expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n"
             + "STARTFILTERS\nMETAL\tclin_sig Contains benign\nENDFILTERS\n\n"
index cf3c7e5..0b5dfdd 100644 (file)
@@ -62,17 +62,17 @@ public class SequenceAnnotationReportTest
             3, 1.2f, "group");
 
     // residuePos == 2 does not match start or end of feature, nothing done:
-    sar.appendFeature(sb, 2, null, sf);
+    sar.appendFeature(sb, 2, null, sf, null);
     assertEquals("123456", sb.toString());
 
     // residuePos == 1 matches start of feature, text appended (but no <br>)
     // feature score is not included
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("123456disulfide bond 1:3", sb.toString());
 
     // residuePos == 3 matches end of feature, text appended
     // <br> is prefixed once sb.length() > 6
-    sar.appendFeature(sb, 3, null, sf);
+    sar.appendFeature(sb, 3, null, sf, null);
     assertEquals("123456disulfide bond 1:3<br>disulfide bond 1:3",
             sb.toString());
   }
@@ -86,7 +86,7 @@ public class SequenceAnnotationReportTest
             Float.NaN, "group");
     sf.setStatus("Confirmed");
 
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3; Fe2-S; (Confirmed)", sb.toString());
   }
 
@@ -100,7 +100,7 @@ public class SequenceAnnotationReportTest
 
     FeatureRendererModel fr = new FeatureRenderer(null);
     Map<String, float[][]> minmax = fr.getMinMax();
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     /*
      * map has no entry for this feature type - score is not shown:
      */
@@ -110,7 +110,7 @@ public class SequenceAnnotationReportTest
      * map has entry for this feature type - score is shown:
      */
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     // <br> is appended to a buffer > 6 in length
     assertEquals("METAL 1 3; Fe2-S<br>METAL 1 3; Fe2-S Score=1.3",
             sb.toString());
@@ -120,7 +120,7 @@ public class SequenceAnnotationReportTest
      */
     minmax.put("METAL", new float[][] { { 2f, 2f }, null });
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -132,7 +132,7 @@ public class SequenceAnnotationReportTest
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3,
             Float.NaN, "group");
 
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -152,7 +152,7 @@ public class SequenceAnnotationReportTest
      * first with no colour by attribute
      */
     FeatureRendererModel fr = new FeatureRenderer(null);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
 
     /*
@@ -163,7 +163,7 @@ public class SequenceAnnotationReportTest
     fc.setAttributeName("Pfam");
     fr.setColour("METAL", fc);
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change
 
     /*
@@ -171,7 +171,7 @@ public class SequenceAnnotationReportTest
      */
     fc.setAttributeName("clinical_significance");
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign",
             sb.toString());
   }
@@ -193,7 +193,7 @@ public class SequenceAnnotationReportTest
     fc.setAttributeName("clinical_significance");
     fr.setColour("METAL", fc);
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
 
     assertEquals(
             "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign",
@@ -209,13 +209,13 @@ public class SequenceAnnotationReportTest
             Float.NaN, "group");
 
     // description is not included if it duplicates type:
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3", sb.toString());
 
     sb.setLength(0);
     sf.setDescription("Metal");
     // test is case-sensitive:
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3; Metal", sb.toString());
   }
 
@@ -228,13 +228,13 @@ public class SequenceAnnotationReportTest
             "<html><body>hello<em>world</em></body></html>", 1, 3,
             Float.NaN, "group");
 
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     // !! strips off </body> but not <body> ??
     assertEquals("METAL 1 3; <body>hello<em>world</em>", sb.toString());
 
     sb.setLength(0);
     sf.setDescription("<br>&kHD>6");
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     // if no <html> tag, html-encodes > and < (only):
     assertEquals("METAL 1 3; &lt;br&gt;&kHD&gt;6", sb.toString());
   }
index a87c160..fb7a4e4 100644 (file)
@@ -1,6 +1,7 @@
 package jalview.io.vcf;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
 
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
@@ -178,13 +179,18 @@ public class VCFLoaderTest
       }
     }
     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.NONSYNONYMOUS_VARIANT);
-    assertEquals(sf.getDescription(), "p.Ser1Thr");
+
+    /*
+     * JAL-3187 don't precompute protein features, do dynamically instead
+     */
+    assertTrue(proteinFeatures.isEmpty());
+    // assertEquals(proteinFeatures.size(), 1);
+    // sf = proteinFeatures.get(0);
+    // assertEquals(sf.getFeatureGroup(), "VCF");
+    // assertEquals(sf.getBegin(), 1);
+    // assertEquals(sf.getEnd(), 1);
+    // assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    // assertEquals(sf.getDescription(), "p.Ser1Thr");
   }
 
   private File makeVcf() throws IOException
@@ -445,13 +451,17 @@ public class VCFLoaderTest
       }
     }
     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.NONSYNONYMOUS_VARIANT);
-    assertEquals(sf.getDescription(), "p.Ala6Gly");
+    /*
+     * JAL-3187 don't precompute protein features, do dynamically instead
+     */
+    assertTrue(proteinFeatures.isEmpty());
+    // assertEquals(proteinFeatures.size(), 1);
+    // sf = proteinFeatures.get(0);
+    // assertEquals(sf.getFeatureGroup(), "VCF");
+    // assertEquals(sf.getBegin(), 6);
+    // assertEquals(sf.getEnd(), 6);
+    // assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    // assertEquals(sf.getDescription(), "p.Ala6Gly");
   }
 
   /**
@@ -494,6 +504,7 @@ public class VCFLoaderTest
     // gene features include Consequence for all transcripts
     Map map = (Map) sf.getValue("CSQ");
     assertEquals(map.size(), 9);
+    assertEquals(map.get("PolyPhen"), "Bad");
 
     sf = geneFeatures.get(1);
     assertEquals(sf.getBegin(), 5);
@@ -503,6 +514,7 @@ public class VCFLoaderTest
     assertEquals(sf.getValue("alleles"), "C,T");
     map = (Map) sf.getValue("CSQ");
     assertEquals(map.size(), 9);
+    assertEquals(map.get("PolyPhen"), "Bad++"); // %3B%3B decoded
 
     sf = geneFeatures.get(2);
     assertEquals(sf.getBegin(), 9);
@@ -605,20 +617,24 @@ public class VCFLoaderTest
       }
     }
     List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
-    SequenceFeatures.sortFeatures(proteinFeatures, true);
-    assertEquals(proteinFeatures.size(), 2);
-    sf = proteinFeatures.get(0);
-    assertEquals(sf.getFeatureGroup(), "VCF");
-    assertEquals(sf.getBegin(), 1);
-    assertEquals(sf.getEnd(), 1);
-    assertEquals(sf.getType(), SequenceOntologyI.SYNONYMOUS_VARIANT);
-    assertEquals(sf.getDescription(), "agC/agT");
-    sf = proteinFeatures.get(1);
-    assertEquals(sf.getFeatureGroup(), "VCF");
-    assertEquals(sf.getBegin(), 4);
-    assertEquals(sf.getEnd(), 4);
-    assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
-    assertEquals(sf.getDescription(), "p.Glu4Gly");
+    /*
+     * JAL-3187 don't precompute protein features, do dynamically instead
+     */
+    assertTrue(proteinFeatures.isEmpty());
+    // SequenceFeatures.sortFeatures(proteinFeatures, true);
+    // assertEquals(proteinFeatures.size(), 2);
+    // sf = proteinFeatures.get(0);
+    // assertEquals(sf.getFeatureGroup(), "VCF");
+    // assertEquals(sf.getBegin(), 1);
+    // assertEquals(sf.getEnd(), 1);
+    // assertEquals(sf.getType(), SequenceOntologyI.SYNONYMOUS_VARIANT);
+    // assertEquals(sf.getDescription(), "agC/agT");
+    // sf = proteinFeatures.get(1);
+    // assertEquals(sf.getFeatureGroup(), "VCF");
+    // assertEquals(sf.getBegin(), 4);
+    // assertEquals(sf.getEnd(), 4);
+    // assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    // assertEquals(sf.getDescription(), "p.Glu4Gly");
 
     /*
      * verify variant feature(s) added to transcript4
diff --git a/test/jalview/io/vcf/testVcf.dat b/test/jalview/io/vcf/testVcf.dat
deleted file mode 100644 (file)
index 77e070c..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-##fileformat=VCFv4.2
-##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
-##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
-##INFO=<ID=AF_Female,Number=R,Type=Float,Description="Allele Frequency among Female genotypes, for each ALT allele, in the same order as listed">
-##INFO=<ID=AN,Number=1,Type=Integer,Description="Total number of alleles in called genotypes">
-##INFO=<ID=CSQ,Number=.,Type=String,Description="Consequence annotations from Ensembl VEP. Format: Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|PolyPhen">
-##reference=/Homo_sapiens/GRCh38
-#CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
-5      45051610        .       C       A       81.96   RF;AC0  AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
-5      45051614        .       C       T       1666.64 RF      AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
-5      45051618        .       CGG     C       41.94   AC0     AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
-5      45051622        .       C       G,T     224.23  RF;AC0  AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
-5      45051626        .       A       AC,G    433.35  RF;AC0  AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
index 77e070c..8a16a90 100644 (file)
@@ -7,7 +7,7 @@
 ##reference=/Homo_sapiens/GRCh38
 #CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
 5      45051610        .       C       A       81.96   RF;AC0  AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
-5      45051614        .       C       T       1666.64 RF      AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051614        .       C       T       1666.64 RF      AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad%2B%2B
 5      45051618        .       CGG     C       41.94   AC0     AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
 5      45051622        .       C       G,T     224.23  RF;AC0  AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
 5      45051626        .       A       AC,G    433.35  RF;AC0  AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
index 5f1256c..5182ad4 100644 (file)
@@ -51,7 +51,6 @@ import jalview.gui.AlignFrame;
 import jalview.gui.AlignViewport;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.Desktop;
-import jalview.gui.FeatureRenderer;
 import jalview.gui.JvOptionPane;
 import jalview.gui.PCAPanel;
 import jalview.gui.PopupMenu;
@@ -74,6 +73,7 @@ import jalview.structure.StructureImportSettings;
 import jalview.util.MapList;
 import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.Color;
 import java.io.File;
@@ -842,13 +842,16 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     /*
      * create a group with Strand colouring, 30% Conservation
      * and 40% PID threshold
+     * (notice menu action applies to selection group even if mouse click
+     * is at a sequence not in the group)
      */
     SequenceGroup sg = new SequenceGroup();
     sg.addSequence(al.getSequenceAt(0), false);
     sg.setStartRes(15);
     sg.setEndRes(25);
     av.setSelectionGroup(sg);
-    PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null);
+    PopupMenu popupMenu = new PopupMenu(af.alignPanel, al.getSequenceAt(2),
+            null);
     popupMenu.changeColour_actionPerformed(
             JalviewColourScheme.Strand.toString());
     assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
@@ -924,7 +927,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     /*
      * set colour schemes for features
      */
-    FeatureRenderer fr = af.getFeatureRenderer();
+    FeatureRendererModel fr = af.getFeatureRenderer();
     fr.findAllFeatures(true);
 
     // type1: red
index af7c2ed..6b1fbd2 100644 (file)
@@ -11,10 +11,10 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.AlignViewport;
-import jalview.gui.FeatureRenderer;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.Color;
@@ -51,7 +51,7 @@ public class FeatureColourFinderTest
 
   private AlignFrame af;
 
-  private FeatureRenderer fr;
+  private FeatureRendererModel fr;
 
   @BeforeTest(alwaysRun = true)
   public void setUp()