Merge branch 'develop' into Jalview-JS/develop
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 8 Oct 2019 09:49:13 +0000 (10:49 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 8 Oct 2019 09:49:13 +0000 (10:49 +0100)
Conflicts:
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/CrossRef.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/PopupMenu.java

61 files changed:
.classpath
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/CrossRef.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/Sequence.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/features/FeatureAttributes.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/fts/core/FTSRestResponse.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/CrossRefAction.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/IdPanel.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/OverviewCanvas.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/io/FeaturesFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/vcf/VCFLoader.java
src/jalview/jbgui/GPreferences.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 0a7e4b3..a85d514 100644 (file)
                </attributes>
        </classpathentry>
        <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
-       <classpathentry kind="lib" path="help"/>
-       <classpathentry kind="lib" path="resources"/>
-       <classpathentry kind="lib" path="j11lib/apache-mime4j-0.6.jar"/>
-       <classpathentry kind="lib" path="j11lib/axis.jar"/>
-       <classpathentry kind="lib" path="j11lib/biojava-core-4.1.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/biojava-ontology-4.1.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/commons-codec-1.3.jar"/>
-       <classpathentry kind="lib" path="j11lib/commons-compress-1.18.jar"/>
-       <classpathentry kind="lib" path="j11lib/commons-discovery.jar"/>
-       <classpathentry kind="lib" path="j11lib/commons-logging-1.1.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/FastInfoset.jar"/>
-       <classpathentry kind="lib" path="j11lib/getdown-core.jar"/>
-       <classpathentry kind="lib" path="j11lib/gmbal-api-only-MODULE.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-ant-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-bsf-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-cli-commons-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-cli-picocli-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-console-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-datetime-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-dateutil-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-docgenerator-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-groovydoc-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-groovysh-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-jaxb-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-jmx-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-json-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-json-direct-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-jsr223-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-macro-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-nio-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-servlet-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-sql-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-swing-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-templates-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-test-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-test-junit5-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-testng-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/groovy-xml-2.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/htsjdk-2.12.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/httpclient-4.0.3.jar"/>
-       <classpathentry kind="lib" path="j11lib/httpcore-4.0.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/httpmime-4.0.3.jar"/>
-       <classpathentry kind="lib" path="j11lib/intervalstore-v1.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/istack-commons-runtime.jar"/>
-       <classpathentry kind="lib" path="j11lib/jabaws-min-client-2.2.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/java-json.jar"/>
-       <classpathentry kind="lib" path="j11lib/javax.activation-api-1.2.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/javax.annotation-api-1.3.2.jar"/>
-       <classpathentry kind="lib" path="j11lib/javax.jws-api-1.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/javax.servlet-api-4.0.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/javax.xml.rpc-api-1.1.2.jar"/>
-       <classpathentry kind="lib" path="j11lib/javax.xml.soap-api-1.4.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/jaxb-api-2.3.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/jaxb-runtime-2.3.2.jar"/>
-       <classpathentry kind="lib" path="j11lib/jaxws-api-2.3.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/jaxws-rt-java9.jar"/>
-       <classpathentry kind="lib" path="j11lib/jersey-client-1.19.4.jar"/>
-       <classpathentry kind="lib" path="j11lib/jersey-core-1.19.4.jar"/>
-       <classpathentry kind="lib" path="j11lib/jersey-json-1.19.4.jar"/>
-       <classpathentry kind="lib" path="j11lib/jetty-http-9.2.10.v20150310.jar"/>
-       <classpathentry kind="lib" path="j11lib/jetty-io-9.2.10.v20150310.jar"/>
-       <classpathentry kind="lib" path="j11lib/jetty-server-9.2.10.v20150310.jar"/>
-       <classpathentry kind="lib" path="j11lib/jetty-util-9.2.10.v20150310.jar"/>
-       <classpathentry kind="lib" path="j11lib/jfreesvg-2.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/JGoogleAnalytics_0.3.jar"/>
-       <classpathentry kind="lib" path="j11lib/jhall.jar"/>
-       <classpathentry kind="lib" path="j11lib/Jmol-14.29.17.jar"/>
-       <classpathentry kind="lib" path="j11lib/json_simple-1.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/jsoup-1.8.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/jsr311-api-1.1.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/jswingreader-0.3.jar"/>
-       <classpathentry kind="lib" path="j11lib/libquaqua-8.0.jnilib.jar"/>
-       <classpathentry kind="lib" path="j11lib/libquaqua64-8.0.jnilib.jar"/>
-       <classpathentry kind="lib" path="j11lib/log4j-to-slf4j-2.0-rc2.jar"/>
-       <classpathentry kind="lib" path="j11lib/mail-MODULE.jar"/>
-       <classpathentry kind="lib" path="j11lib/miglayout-4.0-swing.jar"/>
-       <classpathentry kind="lib" path="j11lib/mimepull-1.9.11.jar"/>
-       <classpathentry kind="lib" path="j11lib/policy-2.7.6.jar"/>
-       <classpathentry kind="lib" path="j11lib/quaqua-filechooser-only-8.0.jar"/>
-       <classpathentry kind="lib" path="j11lib/regex.jar"/>
-       <classpathentry kind="lib" path="j11lib/saaj-impl.jar"/>
-       <classpathentry kind="lib" path="j11lib/slf4j-api-1.7.26.jar"/>
-       <classpathentry kind="lib" path="j11lib/slf4j-log4j12-1.7.26.jar"/>
-       <classpathentry kind="lib" path="j11lib/stax-ex-1.8.1.jar"/>
-       <classpathentry kind="lib" path="j11lib/stax2-api-4.2.jar"/>
-       <classpathentry kind="lib" path="j11lib/streambuffer-1.5.7.jar"/>
-       <classpathentry kind="lib" path="j11lib/txw2-2.3.2.jar"/>
-       <classpathentry kind="lib" path="j11lib/vamsas-client.jar"/>
-       <classpathentry kind="lib" path="j11lib/VAqua5-patch.jar"/>
-       <classpathentry kind="lib" path="j11lib/VARNAv3-93.jar"/>
-       <classpathentry kind="lib" path="j11lib/wsdl4j-1.6.3.jar"/>
-       <classpathentry kind="lib" path="j11lib/xercesImpl.jar"/>
-       <classpathentry kind="lib" path="utils/ant-contrib-1.0b3.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/axis-ant.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/classgraph-4.1.6.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/hamcrest-core-1.3.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/jhall.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/jhindexer.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/junit-4.12.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/proguard_5.3.3.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/roxes-ant-tasks-1.2-2004-01-30.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/bsh-2.0b4.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/guava-base-r03.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/guava-collections-r03.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/jcommander.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/junit-4.12.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/snakeyaml.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/testng-sources.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/testnglibs/testng.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="lib" path="utils/wsdl4j.jar">
-               <attributes>
-                       <attribute name="test" value="true"/>
-               </attributes>
-       </classpathentry>
        <classpathentry kind="output" path="bin/main"/>
 </classpath>
index b137a88..43886a3 100644 (file)
@@ -1401,3 +1401,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 5008c62..9bdf091 100644 (file)
@@ -1403,3 +1403,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 9b8d220..4cd1fc4 100644 (file)
@@ -20,8 +20,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;
@@ -37,7 +36,6 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.SequenceFeatures;
-import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
@@ -45,10 +43,7 @@ import jalview.util.DBRefUtils;
 import jalview.util.IntRangeComparator;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
-import jalview.util.StringUtils;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -1607,7 +1602,7 @@ public class AlignmentUtils
     {
       for (int ix = 0, nx = xrefs.size(); ix < nx; ix++)
       {
-         DBRefEntry xref = xrefs.get(ix);
+        DBRefEntry xref = xrefs.get(ix);
         String xrefName = xref.getSource() + "|" + xref.getAccessionId();
         // case-insensitive test, consistent with DBRefEntry.equalRef()
         if (xrefName.equalsIgnoreCase(name))
@@ -1747,8 +1742,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());
@@ -1985,39 +1982,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());
 
-    int newPos = 0;
-    for (int[] range : fromRanges)
+    SequenceI newSeq = null;
+
+    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])
         {
-          newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 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
+        {
+          // 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());
@@ -2080,8 +2099,8 @@ public class AlignmentUtils
     {
       for (int ib = 0, nb = refs.size(); ib < nb; ib++)
       {
-         DBRefEntry dbr = refs.get(ib);
-         MapList map;
+        DBRefEntry dbr = refs.get(ib);
+        MapList map;
         if (dbr.hasMap() && (map = dbr.getMap().getMap()).isTripletMap())
         {
           // check if map is the CDS mapping
@@ -2101,14 +2120,14 @@ public class AlignmentUtils
     // and generate appropriate mappings
     for (int ic = 0, nc = direct.size(); ic < nc; ic++)
     {
-       DBRefEntry cdsref = direct.get(ic);
-       Mapping m = cdsref.getMap();
+      DBRefEntry cdsref = direct.get(ic);
+      Mapping m = cdsref.getMap();
       // clone maplist and mapping
       MapList cdsposmap = new MapList(
               Arrays.asList(new int[][]
               { new int[] { cdsSeq.getStart(), cdsSeq.getEnd() } }),
               m.getMap().getToRanges(), 3, 1);
-      Mapping cdsmap = new Mapping(m.getTo(),m.getMap());
+      Mapping cdsmap = new Mapping(m.getTo(), m.getMap());
 
       // create dbref
       DBRefEntry newref = new DBRefEntry(cdsref.getSource(),
@@ -2163,6 +2182,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
@@ -2334,7 +2357,7 @@ public class AlignmentUtils
        }
       } catch (NumberFormatException e)
       {
-        // SwingJS -- need to avoid these.
+        // leave as zero
       }
       /*
        * phase > 0 on first codon means 5' incomplete - skip to the start
@@ -2368,393 +2391,6 @@ public class AlignmentUtils
   }
 
   /**
-   * Maps exon features from dna to protein, and computes variants in peptide
-   * product generated by variants in dna, and adds them as sequence_variant
-   * features on the protein sequence. Returns the number of variant features
-   * added.
-   * 
-   * @param dnaSeq
-   * @param peptide
-   * @param dnaToProtein
-   */
-  public static int computeProteinFeatures(SequenceI dnaSeq,
-          SequenceI peptide, MapList dnaToProtein)
-  {
-    while (dnaSeq.getDatasetSequence() != null)
-    {
-      dnaSeq = dnaSeq.getDatasetSequence();
-    }
-    while (peptide.getDatasetSequence() != null)
-    {
-      peptide = peptide.getDatasetSequence();
-    }
-
-    transferFeatures(dnaSeq, peptide, dnaToProtein, SequenceOntologyI.EXON);
-
-    /*
-     * compute protein variants from dna variants and codon mappings;
-     * NB - alternatively we could retrieve this using the REST service e.g.
-     * http://rest.ensembl.org/overlap/translation
-     * /ENSP00000288602?feature=transcript_variation;content-type=text/xml
-     * which would be a bit slower but possibly more reliable
-     */
-
-    /*
-     * build a map with codon variations for each potentially varying peptide
-     */
-    LinkedHashMap<Integer, List<DnaVariant>[]> variants = buildDnaVariantsMap(
-            dnaSeq, dnaToProtein);
-
-    /*
-     * scan codon variations, compute peptide variants and add to peptide sequence
-     */
-    int count = 0;
-    for (Entry<Integer, List<DnaVariant>[]> variant : variants.entrySet())
-    {
-      int peptidePos = variant.getKey();
-      List<DnaVariant>[] codonVariants = variant.getValue();
-      count += computePeptideVariants(peptide, peptidePos, codonVariants);
-    }
-
-    return count;
-  }
-
-  /**
-   * 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
-   * @param peptidePos
-   *          the position to compute peptide variants for
-   * @param codonVariants
-   *          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));
-    int count = 0;
-    String base1 = codonVariants[0].get(0).base;
-    String base2 = codonVariants[1].get(0).base;
-    String base3 = codonVariants[2].get(0).base;
-
-    /*
-     * variants in first codon base
-     */
-    for (DnaVariant dnavar : codonVariants[0])
-    {
-      if (dnavar.variant != null)
-      {
-        String alleles = (String) dnavar.variant.getValue(Gff3Helper.ALLELES);
-        if (alleles != null)
-        {
-          for (String base : alleles.split(","))
-          {
-            if (!base1.equalsIgnoreCase(base))
-            {
-              String codon = base.toUpperCase() + base2.toLowerCase()
-                      + base3.toLowerCase();
-              String canonical = base1.toUpperCase() + base2.toLowerCase()
-                      + base3.toLowerCase();
-              if (addPeptideVariant(peptide, peptidePos, residue, dnavar,
-                      codon, canonical))
-              {
-                count++;
-              }
-            }
-          }
-        }
-      }
-    }
-
-    /*
-     * variants in second codon base
-     */
-    for (DnaVariant var : codonVariants[1])
-    {
-      if (var.variant != null)
-      {
-        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
-        if (alleles != null)
-        {
-          for (String base : alleles.split(","))
-          {
-            if (!base2.equalsIgnoreCase(base))
-            {
-              String codon = base1.toLowerCase() + base.toUpperCase()
-                      + base3.toLowerCase();
-              String canonical = base1.toLowerCase() + base2.toUpperCase()
-                      + base3.toLowerCase();
-              if (addPeptideVariant(peptide, peptidePos, residue, var,
-                      codon, canonical))
-              {
-                count++;
-              }
-            }
-          }
-        }
-      }
-    }
-
-    /*
-     * variants in third codon base
-     */
-    for (DnaVariant var : codonVariants[2])
-    {
-      if (var.variant != null)
-      {
-        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
-        if (alleles != null)
-        {
-          for (String base : alleles.split(","))
-          {
-            if (!base3.equalsIgnoreCase(base))
-            {
-              String codon = base1.toLowerCase() + base2.toLowerCase()
-                      + base.toUpperCase();
-              String canonical = base1.toLowerCase() + base2.toLowerCase()
-                      + base3.toUpperCase();
-              if (addPeptideVariant(peptide, peptidePos, residue, var,
-                      codon, canonical))
-              {
-                count++;
-              }
-            }
-          }
-        }
-      }
-    }
-
-    return count;
-  }
-
-  /**
-   * Helper method that adds a peptide variant feature. ID and
-   * clinical_significance attributes of the dna variant (if present) are copied
-   * to the new feature.
-   * 
-   * @param peptide
-   * @param peptidePos
-   * @param residue
-   * @param var
-   * @param codon
-   *          the variant codon e.g. aCg
-   * @param canonical
-   *          the 'normal' codon e.g. aTg
-   * @return true if a feature was added, else false
-   */
-  static boolean addPeptideVariant(SequenceI peptide, int peptidePos,
-          String residue, DnaVariant var, String codon, String canonical)
-  {
-    /*
-     * get peptide translation of codon e.g. GAT -> D
-     * note that variants which are not single alleles,
-     * e.g. multibase variants or HGMD_MUTATION etc
-     * are currently ignored here
-     */
-    String trans = codon.contains("-") ? null
-            : (codon.length() > CODON_LENGTH ? null
-                    : ResidueProperties.codonTranslate(codon));
-    if (trans == null)
-    {
-      return false;
-    }
-    String desc = canonical + "/" + codon;
-    String featureType = "";
-    if (trans.equals(residue))
-    {
-      featureType = SequenceOntologyI.SYNONYMOUS_VARIANT;
-    }
-    else if (ResidueProperties.STOP.equals(trans))
-    {
-      featureType = SequenceOntologyI.STOP_GAINED;
-    }
-    else
-    {
-      String residue3Char = StringUtils
-              .toSentenceCase(ResidueProperties.aa2Triplet.get(residue));
-      String trans3Char = StringUtils
-              .toSentenceCase(ResidueProperties.aa2Triplet.get(trans));
-      desc = "p." + residue3Char + peptidePos + trans3Char;
-      featureType = SequenceOntologyI.NONSYNONYMOUS_VARIANT;
-    }
-    SequenceFeature sf = new SequenceFeature(featureType, desc, peptidePos,
-            peptidePos, var.getSource());
-
-    StringBuilder attributes = new StringBuilder(32);
-    String id = (String) var.variant.getValue(VARIANT_ID);
-    if (id != null)
-    {
-      if (id.startsWith(SEQUENCE_VARIANT))
-      {
-        id = id.substring(SEQUENCE_VARIANT.length());
-      }
-      sf.setValue(VARIANT_ID, id);
-      attributes.append(VARIANT_ID).append("=").append(id);
-      // TODO handle other species variants JAL-2064
-      StringBuilder link = new StringBuilder(32);
-      try
-      {
-        link.append(desc).append(" ").append(id).append(
-                "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
-                .append(URLEncoder.encode(id, "UTF-8"));
-        sf.addLink(link.toString());
-      } catch (UnsupportedEncodingException e)
-      {
-        // as if
-      }
-    }
-    String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
-    if (clinSig != null)
-    {
-      sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
-      attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
-              .append(clinSig);
-    }
-    peptide.addSequenceFeature(sf);
-    if (attributes.length() > 0)
-    {
-      sf.setAttributes(attributes.toString());
-    }
-    return true;
-  }
-
-  /**
-   * Builds a map whose key is position in the protein sequence, and value is a
-   * list of the base and all variants for each corresponding codon position.
-   * <p>
-   * This depends on dna variants being held as a comma-separated list as
-   * property "alleles" on variant features.
-   * 
-   * @param dnaSeq
-   * @param dnaToProtein
-   * @return
-   */
-  @SuppressWarnings("unchecked")
-  static LinkedHashMap<Integer, List<DnaVariant>[]> buildDnaVariantsMap(
-          SequenceI dnaSeq, MapList dnaToProtein)
-  {
-    /*
-     * map from peptide position to all variants of the codon which codes for it
-     * LinkedHashMap ensures we keep the peptide features in sequence order
-     */
-    LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<>();
-
-    List<SequenceFeature> dnaFeatures = dnaSeq.getFeatures()
-            .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT);
-    if (dnaFeatures.isEmpty())
-    {
-      return variants;
-    }
-
-    int dnaStart = dnaSeq.getStart();
-    int[] lastCodon = null;
-    int lastPeptidePostion = 0;
-
-    /*
-     * build a map of codon variations for peptides
-     */
-    for (SequenceFeature sf : dnaFeatures)
-    {
-      int dnaCol = sf.getBegin();
-      if (dnaCol != sf.getEnd())
-      {
-        // not handling multi-locus variant features
-        continue;
-      }
-
-      /*
-       * ignore variant if not a SNP
-       */
-      String alls = (String) sf.getValue(Gff3Helper.ALLELES);
-      if (alls == null)
-      {
-        continue; // non-SNP VCF variant perhaps - can't process this
-      }
-
-      String[] alleles = alls.toUpperCase().split(",");
-      boolean isSnp = true;
-      for (String allele : alleles)
-      {
-        if (allele.trim().length() > 1)
-        {
-          isSnp = false;
-        }
-      }
-      if (!isSnp)
-      {
-        continue;
-      }
-
-      int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
-      if (mapsTo == null)
-      {
-        // feature doesn't lie within coding region
-        continue;
-      }
-      int peptidePosition = mapsTo[0];
-      List<DnaVariant>[] codonVariants = variants.get(peptidePosition);
-      if (codonVariants == null)
-      {
-        codonVariants = new ArrayList[CODON_LENGTH];
-        codonVariants[0] = new ArrayList<>();
-        codonVariants[1] = new ArrayList<>();
-        codonVariants[2] = new ArrayList<>();
-        variants.put(peptidePosition, codonVariants);
-      }
-
-      /*
-       * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
-       */
-      int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
-              : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
-                      peptidePosition, peptidePosition));
-      lastPeptidePostion = peptidePosition;
-      lastCodon = codon;
-
-      /*
-       * save nucleotide (and any variant) for each codon position
-       */
-      for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++)
-      {
-        String nucleotide = String.valueOf(
-                dnaSeq.getCharAt(codon[codonPos] - dnaStart)).toUpperCase();
-        List<DnaVariant> codonVariant = codonVariants[codonPos];
-        if (codon[codonPos] == dnaCol)
-        {
-          if (!codonVariant.isEmpty()
-                  && codonVariant.get(0).variant == null)
-          {
-            /*
-             * already recorded base value, add this variant
-             */
-            codonVariant.get(0).variant = sf;
-          }
-          else
-          {
-            /*
-             * add variant with base value
-             */
-            codonVariant.add(new DnaVariant(nucleotide, sf));
-          }
-        }
-        else if (codonVariant.isEmpty())
-        {
-          /*
-           * record (possibly non-varying) base value
-           */
-          codonVariant.add(new DnaVariant(nucleotide));
-        }
-      }
-    }
-    return variants;
-  }
-
-  /**
    * Makes an alignment with a copy of the given sequences, adding in any
    * non-redundant sequences which are mapped to by the cross-referenced
    * sequences.
@@ -2774,7 +2410,7 @@ public class AlignmentUtils
     SequenceIdMatcher matcher = new SequenceIdMatcher(seqs);
     if (xrefs != null)
     {
-       // BH 2019.01.25 streamlined this triply nested loop to remove all iterators
+       // BH 2019.01.25 recoded to remove iterators
        
       for (int ix = 0, nx = xrefs.length; ix < nx; ix++)
       {
@@ -2784,9 +2420,9 @@ public class AlignmentUtils
         {
           for (int ir = 0, nir = dbrefs.size(); ir < nir; ir++)
           {
-                 DBRefEntry dbref = dbrefs.get(ir);
-                 Mapping map = dbref.getMap();
-                 SequenceI mto;
+            DBRefEntry dbref = dbrefs.get(ir);
+            Mapping map = dbref.getMap();
+            SequenceI mto;
             if (map == null || (mto = map.getTo()) == null
                     || mto.isProtein() != isProtein)
             {
@@ -2893,10 +2529,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,
@@ -2920,15 +2556,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);
     }
 
     /*
@@ -2936,13 +2579,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)
@@ -2950,6 +2605,12 @@ public class AlignmentUtils
       }
     }
 
+    /*
+     * finally remove gapped columns (e.g. introns)
+     */
+    new RemoveGapColCommand("", unaligned.getSequencesArray(), 0,
+            unaligned.getWidth() - 1, unaligned);
+
     return true;
   }
 
index 00bb63a..c54357e 100644 (file)
@@ -483,7 +483,7 @@ public class CrossRef
   private void removeAlreadyRetrievedSeqs(List<DBRefEntry> sourceRefs,
           boolean fromDna)
   {
-    List<DBRefEntry> dbrSourceSet = new ArrayList<DBRefEntry>(sourceRefs);
+    List<DBRefEntry> dbrSourceSet = new ArrayList<>(sourceRefs);
     List<SequenceI> dsSeqs = dataset.getSequences();
     for (int ids = 0, nds = dsSeqs.size(); ids < nds; ids++)
     {
@@ -929,7 +929,7 @@ public class CrossRef
 
     if (fromDna)
     {
-      AlignmentUtils.computeProteinFeatures(mapFrom, mapTo, mapping);
+      // AlignmentUtils.computeProteinFeatures(mapFrom, mapTo, mapping);
       mappings.addMap(mapFrom, mapTo, mapping);
     }
     else
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 f3fdd67..85ea141 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 5becbc1..776e9ad 100644 (file)
@@ -743,7 +743,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   }
 
   @Override
-  public void highlightSequence(SearchResultsI results)
+  public String highlightSequence(SearchResultsI results)
   {
     if (av.isFollowHighlight())
     {
@@ -760,7 +760,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..0fa03cf
--- /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);
+    int[] codonPositions = codonIntervals == null ? null
+            : MappingUtils.flattenRanges(codonIntervals);
+    if (codonPositions != null && codonPositions.length == 3)
+    {
+      codonPos = codonPositions;
+      baseCodon = new char[3];
+      int cdsStart = fromSeq.getStart();
+      baseCodon[0] = Character
+              .toUpperCase(fromSeq.getCharAt(codonPos[0] - cdsStart));
+      baseCodon[1] = Character
+              .toUpperCase(fromSeq.getCharAt(codonPos[1] - cdsStart));
+      baseCodon[2] = Character
+              .toUpperCase(fromSeq.getCharAt(codonPos[2] - cdsStart));
+    }
+    else
+    {
+      codonPos = null;
+      baseCodon = null;
+    }
+  }
+
+  /**
+   * 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);
+        if (var.contains("p.")) // sanity check
+        {
+          return var;
+        }
+      }
+    }
+
+    /*
+     * otherwise, compute codon and peptide variant
+     */
+    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 950ee05..e0a9d35 100755 (executable)
@@ -472,18 +472,19 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
-   * DOCUMENT ME!
+   * Answers the sequence name, with '/start-end' appended if jvsuffix is true
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
   @Override
   public String getDisplayId(boolean jvsuffix)
   {
-    StringBuffer result = new StringBuffer(name);
-    if (jvsuffix)
+    if (!jvsuffix)
     {
-      result.append("/" + start + "-" + end);
+      return name;
     }
+    StringBuilder result = new StringBuilder(name);
+    result.append("/").append(start).append("-").append(end);
 
     return result.toString();
   }
@@ -1935,15 +1936,6 @@ public class Sequence extends ASequence implements SequenceI
 
     List<SequenceFeature> result = getFeatures().findFeatures(startPos,
             endPos, types);
-    if (datasetSequence != null)
-    {
-      result = datasetSequence.getFeatures().findFeatures(startPos, endPos,
-              types);
-    }
-    else
-    {
-      result = sequenceFeatureStore.findFeatures(startPos, endPos, types);
-    }
 
     /*
      * if end column is gapped, endPos may be to the right, 
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 7acebee..e9a5ece 100644 (file)
@@ -371,4 +371,27 @@ public class FeatureAttributes
     }
     return null;
   }
+
+  /**
+   * Resets all attribute metadata
+   */
+  public void clear()
+  {
+    attributes.clear();
+  }
+
+  /**
+   * Resets attribute metadata for one feature type
+   * 
+   * @param featureType
+   */
+  public void clear(String featureType)
+  {
+    Map<String[], AttributeData> map = attributes.get(featureType);
+    if (map != null)
+    {
+      map.clear();
+    }
+
+  }
 }
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 2e20665..c22b414 100644 (file)
@@ -340,8 +340,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 4698e4d..453152e 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();
@@ -758,7 +759,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("^");
@@ -839,18 +840,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 d9658e4..597bb89 100644 (file)
@@ -21,6 +21,7 @@
 
 package jalview.fts.core;
 
+import jalview.datamodel.SequenceI;
 import jalview.fts.api.FTSData;
 import jalview.fts.api.FTSDataColumnI;
 
@@ -106,7 +107,7 @@ public class FTSRestResponse
       {
         if (colOffset == 1 && columnIndex == 0)
         {
-          return String.class;
+          return SequenceI.class;
         }
         return cols[columnIndex - colOffset].getDataType()
                 .getDataTypeClass();
index 9cb690f..5c2cdd0 100644 (file)
@@ -1065,6 +1065,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
   public int printWrappedAlignment(int pageWidth, int pageHeight, int pageNumber,
           Graphics g) throws PrinterException
   {
+    getSeqPanel().seqCanvas.calculateWrappedGeometry(getWidth(),
+            getHeight());
     int annotationHeight = 0;
     if (av.isShowAnnotation())
     {
@@ -1086,6 +1088,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
     int resWidth = getSeqPanel().seqCanvas
             .getWrappedCanvasWidth(pageWidth - idWidth);
+    av.getRanges().setViewportStartAndWidth(0, resWidth);
 
     int totalHeight = cHeight * (maxwidth / resWidth + 1);
 
@@ -1097,8 +1100,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     /*
      * method: print the whole wrapped alignment, but with a clip region that
      * is restricted to the requested page; this supports selective print of 
-     * single  pages or ranges, (at the cost of some repeated processing in 
-     * the 'normal' case, when all pages are printed)
+     * single pages or ranges, (at the cost of repeated processing in the 
+     * 'normal' case, when all pages are printed)
      */
     g.translate(0, -pageNumber * pageHeight);
 
@@ -1372,6 +1375,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
   }
 
+  /**
+   * Answers the height of the entire alignment in pixels, assuming it is in
+   * wrapped mode
+   * 
+   * @return
+   */
   int getWrappedHeight()
   {
     int seqPanelWidth = getSeqPanel().seqCanvas.getWidth();
@@ -1396,6 +1405,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     int annotationHeight = 0;
     if (av.isShowAnnotation())
     {
+      hgap += SeqCanvas.SEQS_ANNOTATION_GAP;
       annotationHeight = getAnnotationPanel().adjustPanelHeight();
     }
 
index 81960e0..d84287f 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"));
   }
 
@@ -216,14 +247,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;
   }
@@ -269,11 +303,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
@@ -282,7 +375,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
@@ -291,7 +385,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
@@ -300,52 +394,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 974a6e5..2ada4d2 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 9fe2e92..5aa9499 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;
@@ -36,6 +38,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;
@@ -45,6 +48,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;
@@ -145,7 +149,9 @@ public class FeatureSettings extends JPanel
 
   float originalTransparency;
 
-  Map<String, FeatureMatcherSetI> originalFilters;
+  private ViewStyleI originalViewStyle;
+
+  private Map<String, FeatureMatcherSetI> originalFilters;
 
   final JInternalFrame frame;
 
@@ -192,6 +198,7 @@ public class FeatureSettings extends JPanel
     transparency.setMaximum(100 - originalTransparencyAsPercent);
 
     originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
+    originalViewStyle = new ViewStyle(af.viewport.getViewStyle());
 
     try
     {
@@ -277,20 +284,23 @@ public class FeatureSettings extends JPanel
       @Override
       public void mousePressed(MouseEvent evt)
       {
-        selectedRow = table.rowAtPoint(evt.getPoint());
+        Point pt = evt.getPoint();
+        selectedRow = table.rowAtPoint(pt);
         String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
         if (evt.isPopupTrigger())
         {
           Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
           showPopupMenu(selectedRow, type, colour, evt.getPoint());
         }
-        else if (evt.getClickCount() == 2)
+        else if (evt.getClickCount() == 2
+                && table.columnAtPoint(pt) == TYPE_COLUMN)
         {
           boolean invertSelection = evt.isAltDown();
           boolean toggleSelection = Platform.isControlDown(evt);
           boolean extendSelection = evt.isShiftDown();
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
                   invertSelection, extendSelection, toggleSelection, type);
+          fr.ap.av.sendSelection();
         }
       }
 
@@ -379,7 +389,7 @@ public class FeatureSettings extends JPanel
                       javax.swing.event.InternalFrameEvent evt)
               {
                 fr.removePropertyChangeListener(change);
-              };
+              }
             });
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
     inConstruction = false;
@@ -513,6 +523,7 @@ public class FeatureSettings extends JPanel
       {
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
                 false, type);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem clearCols = new JMenuItem(MessageManager
@@ -524,6 +535,7 @@ public class FeatureSettings extends JPanel
       {
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
                 false, type);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem hideCols = new JMenuItem(
@@ -534,6 +546,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(
@@ -544,6 +557,7 @@ public class FeatureSettings extends JPanel
       public void actionPerformed(ActionEvent arg0)
       {
         fr.ap.alignFrame.hideFeatureColumns(type, false);
+        fr.ap.av.sendSelection();
       }
     });
     men.add(selCols);
@@ -607,7 +621,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);
@@ -1166,7 +1180,7 @@ public class FeatureSettings extends JPanel
 
     if (fr.setFeaturePriority(rowData, visibleNew))
     {
-      af.alignPanel.paintAlignment(true, true);
+      refreshDisplay();
     }
   }
 
@@ -1275,6 +1289,7 @@ public class FeatureSettings extends JPanel
         fr.setTransparency(originalTransparency);
         fr.setFeatureFilters(originalFilters);
         updateFeatureRenderer(originalData);
+        af.getViewport().setViewStyle(originalViewStyle);
         close();
       }
     });
@@ -1325,7 +1340,7 @@ public class FeatureSettings extends JPanel
         if (!inConstruction)
         {
           fr.setTransparency((100 - transparency.getValue()) / 100f);
-          af.alignPanel.paintAlignment(true, true);
+          refreshDisplay();
         }
       }
     });
@@ -1334,8 +1349,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);
@@ -1343,8 +1392,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);
@@ -1358,11 +1420,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,
@@ -1499,13 +1577,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 bac9d9b..200911d 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;
@@ -676,7 +677,7 @@ public class FeatureTypeSettings extends JalviewDialog
          */
         if (ap != null)
         {
-          ap.paintAlignment(true, true);
+          refreshDisplay(true);
         }
       }
     });
@@ -892,7 +893,7 @@ public class FeatureTypeSettings extends JalviewDialog
      * save the colour, and repaint stuff
      */
     fr.setColour(featureType, acg);
-    ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
+    refreshDisplay(updateStructsAndOverview);
 
     updateColoursPanel();
   }
@@ -1031,7 +1032,7 @@ public class FeatureTypeSettings extends JalviewDialog
   {
     fr.setColour(featureType, originalColour);
     fr.setFeatureFilter(featureType, originalFilter);
-    ap.paintAlignment(true, true);
+    refreshDisplay(true);
   }
 
   /**
@@ -1780,8 +1781,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);
 
     updateFiltersPanel();
   }
+
+  /**
+   * 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 4057cef..b3e8119 100755 (executable)
@@ -384,32 +384,20 @@ public class IdCanvas extends JPanel implements ViewportListenerI
     int alignmentWidth = alignViewport.getAlignment().getWidth();
     final int alheight = alignViewport.getAlignment().getHeight();
 
-    int annotationHeight = 0;
+    /*
+     * assumption: SeqCanvas.calculateWrappedGeometry has been called
+     */
+    SeqCanvas seqCanvas = alignViewport.getAlignPanel()
+            .getSeqPanel().seqCanvas;
+
+    final int charHeight = alignViewport.getCharHeight();
 
     AnnotationLabels labels = null;
     if (alignViewport.isShowAnnotation())
     {
-      if (ap == null)
-      {
-        ap = new AnnotationPanel(alignViewport);
-      }
-      annotationHeight = ap.adjustPanelHeight();
       labels = new AnnotationLabels(alignViewport);
     }
 
-    final int charHeight = alignViewport.getCharHeight();
-    int hgap = charHeight;
-    if (alignViewport.getScaleAboveWrapped())
-    {
-      hgap += charHeight;
-    }
-
-    /*
-     * height of alignment + gap + annotations (if shown)
-     */
-    int cHeight = alheight * charHeight + hgap
-            + annotationHeight;
-
     ViewportRanges ranges = alignViewport.getRanges();
 
     int rowSize = ranges.getViewportWidth();
@@ -419,7 +407,7 @@ public class IdCanvas extends JPanel implements ViewportListenerI
      * out of visible space, whichever comes first
      */
     boolean hasHiddenRows = alignViewport.hasHiddenRows();
-    int ypos = hgap;
+    int ypos = seqCanvas.wrappedSpaceAboveAlignment;
     int rowStartRes = ranges.getStartRes();
     while ((ypos <= pageHeight) && (rowStartRes < alignmentWidth))
     {
@@ -444,7 +432,7 @@ public class IdCanvas extends JPanel implements ViewportListenerI
         g.translate(0, -ypos - (alheight * charHeight));
       }
 
-      ypos += cHeight;
+      ypos += seqCanvas.wrappedRepeatHeightPx;
       rowStartRes += rowSize;
     }
   }
index 6b312ad..f53d8b3 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;
@@ -436,28 +435,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 ca27a9c..8e67992 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 JPanel
    *          the renderer to transfer feature colouring from
    */
   public void draw(boolean showSequenceFeatures, boolean showAnnotation,
-          FeatureRenderer transferRenderer)
+          FeatureRendererModel transferRenderer)
   {
     miniMe = null;
 
index ad33110..8a37952 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;
@@ -48,12 +49,14 @@ 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.Platform;
 import jalview.util.StringUtils;
 import jalview.util.UrlLink;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -68,6 +71,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;
@@ -89,6 +93,19 @@ import javax.swing.JScrollPane;
  */
 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();
@@ -103,28 +120,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();
@@ -137,18 +140,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();
@@ -165,22 +162,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,
@@ -191,7 +180,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"));
@@ -363,41 +352,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)
   {
-    // /////////////////////////////////////////////////////////
-    // 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
-    // ////////////////////////////////////////////////////////
+    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)
+  {
+    Objects.requireNonNull(seq);
+    this.forIdPanel = fromIdPanel;
     this.ap = alignPanel;
     sequence = seq;
 
@@ -422,9 +425,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,
@@ -449,9 +452,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())
       {
@@ -606,7 +609,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()
         {
@@ -698,27 +701,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);
     }
   }
 
@@ -726,69 +759,121 @@ 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)
   {
     JInternalFrame details;
     if (Platform.isJS())
@@ -800,7 +885,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       // TODO JAL-3026 set style of table correctly for feature details
       JLabel reprt = new JLabel(MessageManager
               .formatMessage("label.html_content", new Object[]
-              { sf.getDetailsReport() }));
+              { sf.getDetailsReport(seqName) }));
       reprt.setBackground(Color.WHITE);
       reprt.setOpaque(true);
       panel.add(reprt, BorderLayout.CENTER);
@@ -818,7 +903,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       // it appears Java's CSS does not support border-collaps :-(
       cap.addStylesheetRule("table { border-collapse: collapse;}");
       cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
-      cap.setText(sf.getDetailsReport());
+      cap.setText(sf.getDetailsReport(seqName));
       details = cap;
     }
     Desktop.addInternalFrame(details,
@@ -837,12 +922,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);
       }
@@ -990,7 +1075,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,
@@ -1058,7 +1143,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         Cache.log.error("Exception for GroupURLLink '" + link + "'", foo);
         continue;
       }
-      ;
       if (!urlLink.isValid())
       {
         Cache.log.error(urlLink.getInvalidMessage());
@@ -1151,7 +1235,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()
     {
@@ -1161,8 +1246,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
@@ -1171,24 +1256,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());
       }
     });
 
@@ -1213,7 +1298,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
@@ -1263,7 +1349,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
@@ -1281,7 +1367,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
@@ -1318,7 +1404,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()
     {
@@ -1328,10 +1414,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)
@@ -1349,7 +1435,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()
     {
@@ -1371,25 +1457,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
@@ -1410,7 +1496,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);
@@ -1424,17 +1510,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);
   }
 
   /**
@@ -1697,11 +1779,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)
   {
     StringBuilder contents = new StringBuilder(128);
@@ -1911,7 +1988,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   }
 
   /**
-   * Shows a dialog where sequence name and description may be edited
+   * 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()
   {
@@ -1983,7 +2062,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       {
         getGroup().setOutlineColour(c);
         refresh();
-      };
+      }
     };
     JalviewColourChooser.showColourChooser(Desktop.getDesktop(),
             title, Color.BLUE, listener);
@@ -2146,25 +2225,6 @@ 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);
-
-  }
-
   /**
    * Shows a dialog where sequence characters may be edited. Any changes are
    * applied, and added as an available 'Undo' item in the edit commands
@@ -2174,16 +2234,16 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   {
     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(),
-                      sg.getEndRes() + 1),
+              seq.getSequenceAsString(sg.getStartRes(), sg.getEndRes() + 1),
               null, MessageManager.getString("label.edit_sequence"), null);
       dialog.showDialog(ap.alignFrame,
               MessageManager.getString("label.edit_sequence"),
index e962709..b27208a 100755 (executable)
@@ -56,8 +56,8 @@ import javax.swing.JPanel;
 @SuppressWarnings("serial")
 public class SeqCanvas extends JPanel implements ViewportListenerI
 {
-  /*
-   * pixels gap between sequences and annotations when in wrapped mode
+  /**
+   * vertical gap in pixels between sequences and annotations when in wrapped mode
    */
   static final int SEQS_ANNOTATION_GAP = 3;
 
@@ -890,35 +890,24 @@ public class SeqCanvas extends JPanel implements ViewportListenerI
           int canvasWidth,
           int canvasHeight, int startRes)
   {
-    int charHeight = av.getCharHeight();
-    int charWidth = av.getCharWidth();
-      
-    // height gap above each panel
-    int hgap = charHeight;
-    if (av.getScaleAboveWrapped())
-    {
-      hgap += charHeight;
-    }
-
-    int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
-            / charWidth;
-    int cHeight = av.getAlignment().getHeight() * charHeight;
-
-    int startx = startRes;
-    int endx;
-    int ypos = hgap; // vertical offset
-    int maxwidth = av.getAlignment().getVisibleWidth();
-
     // chop the wrapped alignment extent up into panel-sized blocks and treat
     // each block as if it were a block from an unwrapped alignment
     g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
             BasicStroke.JOIN_ROUND, 3f, new float[]
             { 5f, 3f }, 0f));
     g.setColor(Color.RED);
+
+    int charWidth = av.getCharWidth();
+    int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
+            / charWidth;
+    int startx = startRes;
+    int maxwidth = av.getAlignment().getVisibleWidth();
+    int ypos = wrappedSpaceAboveAlignment;
+
     while ((ypos <= canvasHeight) && (startx < maxwidth))
     {
       // set end value to be start + width, or maxwidth, whichever is smaller
-      endx = startx + cWidth - 1;
+      int endx = startx + cWidth - 1;
 
       if (endx > maxwidth)
       {
@@ -926,22 +915,24 @@ public class SeqCanvas extends JPanel implements ViewportListenerI
       }
 
       g.translate(labelWidthWest, 0);
-
       drawUnwrappedSelection(g, group, startx, endx, 0,
               av.getAlignment().getHeight() - 1,
               ypos);
-
       g.translate(-labelWidthWest, 0);
 
-      // update vertical offset
-      ypos += cHeight + getAnnotationHeight() + hgap;
+      ypos += wrappedRepeatHeightPx;
 
-      // update horizontal offset
       startx += cWidth;
     }
     g.setStroke(new BasicStroke());
   }
 
+  /**
+   * Answers zero if annotations are not shown, otherwise recalculates and answers
+   * the total height of all annotation rows in pixels
+   * 
+   * @return
+   */
   int getAnnotationHeight()
   {
     if (!av.isShowAnnotation())
index 109323b..185c5c5 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;
@@ -63,6 +65,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;
 
@@ -841,11 +844,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;
 
@@ -871,6 +874,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
@@ -983,6 +1057,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>
     {
@@ -2171,11 +2266,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 5f68099..f91ea86 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 2381ecf..5451a20 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;
@@ -124,19 +125,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);
     }
   }
 
@@ -149,7 +164,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())
     {
@@ -218,6 +234,15 @@ public class SequenceAnnotationReport
           }
         }
       }
+
+      if (mf != null)
+      {
+        String variants = mf.findProteinVariants(feature);
+        if (!variants.isEmpty())
+        {
+          sb.append(" ").append(variants);
+        }
+      }
     }
   }
 
@@ -372,7 +397,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 990952d..b20fc14 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,11 +22,16 @@ 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.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
@@ -35,8 +39,10 @@ import htsjdk.samtools.SAMException;
 import htsjdk.samtools.SAMSequenceDictionary;
 import htsjdk.samtools.SAMSequenceRecord;
 import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.tribble.TribbleException;
 import htsjdk.variant.variantcontext.Allele;
 import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFConstants;
 import htsjdk.variant.vcf.VCFHeader;
 import htsjdk.variant.vcf.VCFHeaderLine;
 import htsjdk.variant.vcf.VCFHeaderLineCount;
@@ -51,6 +57,31 @@ import htsjdk.variant.vcf.VCFInfoHeaderLine;
  */
 public class VCFLoader
 {
+  private static final String ENCODED_COMMA = "%2C";
+
+  private static final String ENCODED_PERCENT = "%25";
+
+  private static final String ENCODED_EQUALS = "%3D";
+
+  private static final String ENCODED_SEMICOLON = "%3B";
+
+  private static final String ENCODED_COLON = "%3A";
+
+  private static final String UTF_8 = "UTF-8";
+
+  /*
+   * Jalview feature attributes for VCF fixed column data
+   */
+  private static final String VCF_POS = "POS";
+
+  private static final String VCF_ID = "ID";
+
+  private static final String VCF_QUAL = "QUAL";
+
+  private static final String VCF_FILTER = "FILTER";
+
+  private static final String NO_VALUE = VCFConstants.MISSING_VALUE_v4; // '.'
+
   private static final String DEFAULT_SPECIES = "homo_sapiens";
 
   /**
@@ -209,6 +240,12 @@ public class VCFLoader
    */
   Map<Integer, String> vepFieldsOfInterest;
 
+  /*
+   * key:value for which rejected data has been seen
+   * (the error is logged only once for each combination)
+   */
+  private Set<String> badData;
+
   /**
    * Constructor given a VCF file
    * 
@@ -654,7 +691,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
       {
@@ -839,24 +877,35 @@ public class VCFLoader
     {
       int vcfStart = Math.min(range[0], range[1]);
       int vcfEnd = Math.max(range[0], range[1]);
-      CloseableIterator<VariantContext> variants = reader
-              .query(map.chromosome, vcfStart, vcfEnd);
-      while (variants.hasNext())
+      try
       {
-        VariantContext variant = variants.next();
+        CloseableIterator<VariantContext> variants = reader
+                .query(map.chromosome, vcfStart, vcfEnd);
+        while (variants.hasNext())
+        {
+          VariantContext variant = variants.next();
 
-        int[] featureRange = map.map.locateInFrom(variant.getStart(),
-                variant.getEnd());
+          int[] featureRange = map.map.locateInFrom(variant.getStart(),
+                  variant.getEnd());
 
-        if (featureRange != null)
-        {
-          int featureStart = Math.min(featureRange[0], featureRange[1]);
-          int featureEnd = Math.max(featureRange[0], featureRange[1]);
-          count += addAlleleFeatures(seq, variant, featureStart, featureEnd,
-                  forwardStrand);
+          if (featureRange != null)
+          {
+            int featureStart = Math.min(featureRange[0], featureRange[1]);
+            int featureEnd = Math.max(featureRange[0], featureRange[1]);
+            count += addAlleleFeatures(seq, variant, featureStart,
+                    featureEnd, forwardStrand);
+          }
         }
+        variants.close();
+      } catch (TribbleException e)
+      {
+        /*
+         * RuntimeException throwable by htsjdk
+         */
+        String msg = String.format("Error reading VCF for %s:%d-%d: %s ",
+                map.chromosome, vcfStart, vcfEnd);
+        Cache.log.error(msg);
       }
-      variants.close();
     }
 
     return count;
@@ -986,7 +1035,20 @@ public class VCFLoader
             featureEnd, FEATURE_GROUP_VCF);
     sf.setSource(sourceId);
 
-    sf.setValue(Gff3Helper.ALLELES, alleles);
+    /*
+     * save the derived alleles as a named attribute; this will be
+     * needed when Jalview computes derived peptide variants
+     */
+    addFeatureAttribute(sf, Gff3Helper.ALLELES, alleles);
+
+    /*
+     * add selected VCF fixed column data as feature attributes
+     */
+    addFeatureAttribute(sf, VCF_POS, String.valueOf(variant.getStart()));
+    addFeatureAttribute(sf, VCF_ID, variant.getID());
+    addFeatureAttribute(sf, VCF_QUAL,
+            String.valueOf(variant.getPhredScaledQual()));
+    addFeatureAttribute(sf, VCF_FILTER, getFilter(variant));
 
     addAlleleProperties(variant, sf, altAlleleIndex, consequence);
 
@@ -996,6 +1058,53 @@ public class VCFLoader
   }
 
   /**
+   * Answers the VCF FILTER value for the variant - or an approximation to it.
+   * This field is either PASS, or a semi-colon separated list of filters not
+   * passed. htsjdk saves filters as a HashSet, so the order when reassembled into
+   * a list may be different.
+   * 
+   * @param variant
+   * @return
+   */
+  String getFilter(VariantContext variant)
+  {
+    Set<String> filters = variant.getFilters();
+    if (filters.isEmpty())
+    {
+      return NO_VALUE;
+    }
+    Iterator<String> iterator = filters.iterator();
+    String first = iterator.next();
+    if (filters.size() == 1)
+    {
+      return first;
+    }
+
+    StringBuilder sb = new StringBuilder(first);
+    while (iterator.hasNext())
+    {
+      sb.append(";").append(iterator.next());
+    }
+
+    return sb.toString();
+  }
+
+  /**
+   * Adds one feature attribute unless the value is null, empty or '.'
+   * 
+   * @param sf
+   * @param key
+   * @param value
+   */
+  void addFeatureAttribute(SequenceFeature sf, String key, String value)
+  {
+    if (value != null && !value.isEmpty() && !NO_VALUE.equals(value))
+    {
+      sf.setValue(key, value);
+    }
+  }
+
+  /**
    * Determines the Sequence Ontology term to use for the variant feature type in
    * Jalview. The default is 'sequence_variant', but a more specific term is used
    * if:
@@ -1189,14 +1298,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') 
@@ -1233,14 +1334,110 @@ public class VCFLoader
        * take the index'th value
        */
       String value = getAttributeValue(variant, key, index);
-      if (value != null)
+      if (value != null && isValid(variant, key, value))
       {
-        sf.setValue(key, value);
+        value = decodeSpecialCharacters(value);
+        addFeatureAttribute(sf, key, value);
       }
     }
   }
 
   /**
+   * Decodes colon, semicolon, equals sign, percent sign, comma to their decoded
+   * form. The VCF specification (para 1.2) requires these to be encoded where not
+   * used with their special meaning in the VCF syntax. Note that general URL
+   * decoding should not be applied, since this would incorrectly decode (for
+   * example) a '+' sign.
+   * 
+   * @param value
+   * @return
+   */
+  protected static String decodeSpecialCharacters(String value)
+  {
+    /*
+     * avoid regex compilation if it is not needed!
+     */
+    if (!value.contains(ENCODED_COLON) && !value.contains(ENCODED_SEMICOLON)
+            && !value.contains(ENCODED_EQUALS)
+            && !value.contains(ENCODED_PERCENT)
+            && !value.contains(ENCODED_COMMA))
+    {
+      return value;
+    }
+
+    value = value.replace(ENCODED_COLON, ":")
+            .replace(ENCODED_SEMICOLON, ";").replace(ENCODED_EQUALS, "=")
+            .replace(ENCODED_PERCENT, "%").replace(ENCODED_COMMA, ",");
+    return value;
+  }
+
+  /**
+   * Answers true for '.', null, or an empty value, or if the INFO type is String.
+   * If the INFO type is Integer or Float, answers false if the value is not in
+   * valid format.
+   * 
+   * @param variant
+   * @param infoId
+   * @param value
+   * @return
+   */
+  protected boolean isValid(VariantContext variant, String infoId,
+          String value)
+  {
+    if (value == null || value.isEmpty() || NO_VALUE.equals(value))
+    {
+      return true;
+    }
+    VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(infoId);
+    if (infoHeader == null)
+    {
+      Cache.log.error("Field " + infoId + " has no INFO header");
+      return false;
+    }
+    VCFHeaderLineType infoType = infoHeader.getType();
+    try
+    {
+      if (infoType == VCFHeaderLineType.Integer)
+      {
+        Integer.parseInt(value);
+      }
+      else if (infoType == VCFHeaderLineType.Float)
+      {
+        Float.parseFloat(value);
+      }
+    } catch (NumberFormatException e)
+    {
+      logInvalidValue(variant, infoId, value);
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Logs an error message for malformed data; duplicate messages (same id and
+   * value) are not logged
+   * 
+   * @param variant
+   * @param infoId
+   * @param value
+   */
+  private void logInvalidValue(VariantContext variant, String infoId,
+          String value)
+  {
+    if (badData == null)
+    {
+      badData = new HashSet<>();
+    }
+    String token = infoId + ":" + value;
+    if (!badData.contains(token))
+    {
+      badData.add(token);
+      Cache.log.error(String.format("Invalid VCF data at %s:%d %s=%s",
+              variant.getContig(), variant.getStart(), infoId, value));
+    }
+  }
+
+  /**
    * Inspects CSQ data blocks (consequences) and adds attributes on the sequence
    * feature.
    * <p>
@@ -1288,6 +1485,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 64c0323..6e4b400 100755 (executable)
@@ -2543,7 +2543,7 @@ public class GPreferences extends JPanel
     {
       try
       {
-        i = Integer.parseInt((String) s.getValue());
+        i = ((Integer) s.getValue()).intValue();
       } catch (Exception e)
       {
         Cache.log.error(
index 8f9338d..7f7a210 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;
@@ -1513,11 +1513,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]);
 
@@ -3174,25 +3177,25 @@ public class Jalview2XML
    * @param prefix
    *          a prefix for the temporary file name, must be at least three
    *          characters long
-   * @param origFile
+   * @param suffixModel
    *          null or original file - so new file can be given the same suffix
    *          as the old one
    * @return
    */
   protected String copyJarEntry(jarInputStreamProvider jprovider,
-          String jarEntryName, String prefix, String origFile)
+          String jarEntryName, String prefix, String suffixModel)
   {
     BufferedReader in = null;
     PrintWriter out = null;
     String suffix = ".tmp";
-    if (origFile == null)
+    if (suffixModel == null)
     {
-      origFile = jarEntryName;
+      suffixModel = jarEntryName;
     }
-    int sfpos = origFile.lastIndexOf(".");
-    if (sfpos > -1 && sfpos < (origFile.length() - 3))
+    int sfpos = suffixModel.lastIndexOf(".");
+    if (sfpos > -1 && sfpos < (suffixModel.length() - 1))
     {
-      suffix = "." + origFile.substring(sfpos + 1);
+      suffix = "." + suffixModel.substring(sfpos + 1);
     }
     try
     {
@@ -4458,7 +4461,7 @@ public class Jalview2XML
      */
     String viewerJarEntryName = getViewerJarEntryName(data.getViewId());
     chimeraSessionFile = copyJarEntry(jprovider, viewerJarEntryName,
-            "chimera", null);
+            "chimera", ".py");
 
     Set<Entry<File, StructureData>> fileData = data.getFileData()
             .entrySet();
@@ -5044,11 +5047,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 1744467..798b07a 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 536f767..4f399f0 100644 (file)
@@ -2720,6 +2720,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 54bb4ee..459a28b 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;
@@ -321,12 +327,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);
       }
@@ -984,7 +990,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
@@ -999,7 +1005,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)
@@ -1012,12 +1018,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)
     {
@@ -1185,6 +1190,99 @@ 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);
+
+    /*
+     * fudge: if no mapping found, check the complementary alignment
+     * todo: only store in one place? StructureSelectionManager?
+     */
+    if (mappings.isEmpty())
+    {
+      mappings = this.av.getCodingComplement().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 d54ff8a..9eec241 100644 (file)
@@ -1871,408 +1871,6 @@ public class AlignmentUtilsTests
   }
 
   /**
-   * Test the method that computes a map of codon variants for each protein
-   * position from "sequence_variant" features on dna
-   */
-  @Test(groups = "Functional")
-  public void testBuildDnaVariantsMap()
-  {
-    SequenceI dna = new Sequence("dna", "atgAAATTTGGGCCCtag");
-    MapList map = new MapList(new int[] { 1, 18 }, new int[] { 1, 5 }, 3, 1);
-
-    /*
-     * first with no variants on dna
-     */
-    LinkedHashMap<Integer, List<DnaVariant>[]> variantsMap = AlignmentUtils
-            .buildDnaVariantsMap(dna, map);
-    assertTrue(variantsMap.isEmpty());
-
-    /*
-     * single allele codon 1, on base 1
-     */
-    SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1,
-            0f, null);
-    sf1.setValue("alleles", "T");
-    sf1.setValue("ID", "sequence_variant:rs758803211");
-    dna.addSequenceFeature(sf1);
-
-    /*
-     * two alleles codon 2, on bases 2 and 3 (distinct variants)
-     */
-    SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 5, 5,
-            0f, null);
-    sf2.setValue("alleles", "T");
-    sf2.setValue("ID", "sequence_variant:rs758803212");
-    dna.addSequenceFeature(sf2);
-    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 6, 6,
-            0f, null);
-    sf3.setValue("alleles", "G");
-    sf3.setValue("ID", "sequence_variant:rs758803213");
-    dna.addSequenceFeature(sf3);
-
-    /*
-     * two alleles codon 3, both on base 2 (one variant)
-     */
-    SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 8, 8,
-            0f, null);
-    sf4.setValue("alleles", "C, G");
-    sf4.setValue("ID", "sequence_variant:rs758803214");
-    dna.addSequenceFeature(sf4);
-
-    // no alleles on codon 4
-
-    /*
-     * alleles on codon 5 on all 3 bases (distinct variants)
-     */
-    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 13,
-            13, 0f, null);
-    sf5.setValue("alleles", "C, G"); // (C duplicates given base value)
-    sf5.setValue("ID", "sequence_variant:rs758803215");
-    dna.addSequenceFeature(sf5);
-    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 14,
-            14, 0f, null);
-    sf6.setValue("alleles", "g, a"); // should force to upper-case
-    sf6.setValue("ID", "sequence_variant:rs758803216");
-    dna.addSequenceFeature(sf6);
-
-    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 15,
-            15, 0f, null);
-    sf7.setValue("alleles", "A, T");
-    sf7.setValue("ID", "sequence_variant:rs758803217");
-    dna.addSequenceFeature(sf7);
-
-    /*
-     * build map - expect variants on positions 1, 2, 3, 5
-     */
-    variantsMap = AlignmentUtils.buildDnaVariantsMap(dna, map);
-    assertEquals(4, variantsMap.size());
-
-    /*
-     * protein residue 1: variant on codon (ATG) base 1, not on 2 or 3
-     */
-    List<DnaVariant>[] pep1Variants = variantsMap.get(1);
-    assertEquals(3, pep1Variants.length);
-    assertEquals(1, pep1Variants[0].size());
-    assertEquals("A", pep1Variants[0].get(0).base); // codon[1] base
-    assertSame(sf1, pep1Variants[0].get(0).variant); // codon[1] variant
-    assertEquals(1, pep1Variants[1].size());
-    assertEquals("T", pep1Variants[1].get(0).base); // codon[2] base
-    assertNull(pep1Variants[1].get(0).variant); // no variant here
-    assertEquals(1, pep1Variants[2].size());
-    assertEquals("G", pep1Variants[2].get(0).base); // codon[3] base
-    assertNull(pep1Variants[2].get(0).variant); // no variant here
-
-    /*
-     * protein residue 2: variants on codon (AAA) bases 2 and 3
-     */
-    List<DnaVariant>[] pep2Variants = variantsMap.get(2);
-    assertEquals(3, pep2Variants.length);
-    assertEquals(1, pep2Variants[0].size());
-    // codon[1] base recorded while processing variant on codon[2]
-    assertEquals("A", pep2Variants[0].get(0).base);
-    assertNull(pep2Variants[0].get(0).variant); // no variant here
-    // codon[2] base and variant:
-    assertEquals(1, pep2Variants[1].size());
-    assertEquals("A", pep2Variants[1].get(0).base);
-    assertSame(sf2, pep2Variants[1].get(0).variant);
-    // codon[3] base was recorded when processing codon[2] variant
-    // and then the variant for codon[3] added to it
-    assertEquals(1, pep2Variants[2].size());
-    assertEquals("A", pep2Variants[2].get(0).base);
-    assertSame(sf3, pep2Variants[2].get(0).variant);
-
-    /*
-     * protein residue 3: variants on codon (TTT) base 2 only
-     */
-    List<DnaVariant>[] pep3Variants = variantsMap.get(3);
-    assertEquals(3, pep3Variants.length);
-    assertEquals(1, pep3Variants[0].size());
-    assertEquals("T", pep3Variants[0].get(0).base); // codon[1] base
-    assertNull(pep3Variants[0].get(0).variant); // no variant here
-    assertEquals(1, pep3Variants[1].size());
-    assertEquals("T", pep3Variants[1].get(0).base); // codon[2] base
-    assertSame(sf4, pep3Variants[1].get(0).variant); // codon[2] variant
-    assertEquals(1, pep3Variants[2].size());
-    assertEquals("T", pep3Variants[2].get(0).base); // codon[3] base
-    assertNull(pep3Variants[2].get(0).variant); // no variant here
-
-    /*
-     * three variants on protein position 5
-     */
-    List<DnaVariant>[] pep5Variants = variantsMap.get(5);
-    assertEquals(3, pep5Variants.length);
-    assertEquals(1, pep5Variants[0].size());
-    assertEquals("C", pep5Variants[0].get(0).base); // codon[1] base
-    assertSame(sf5, pep5Variants[0].get(0).variant); // codon[1] variant
-    assertEquals(1, pep5Variants[1].size());
-    assertEquals("C", pep5Variants[1].get(0).base); // codon[2] base
-    assertSame(sf6, pep5Variants[1].get(0).variant); // codon[2] variant
-    assertEquals(1, pep5Variants[2].size());
-    assertEquals("C", pep5Variants[2].get(0).base); // codon[3] base
-    assertSame(sf7, pep5Variants[2].get(0).variant); // codon[3] variant
-  }
-
-  /**
-   * Tests for the method that computes all peptide variants given codon
-   * variants
-   */
-  @Test(groups = "Functional")
-  public void testComputePeptideVariants()
-  {
-    /*
-     * scenario: AAATTTCCC codes for KFP
-     * variants:
-     *           GAA -> E             source: Ensembl
-     *           CAA -> Q             source: dbSNP
-     *           TAA -> STOP          source: dnSNP
-     *           AAG synonymous       source: COSMIC
-     *           AAT -> N             source: Ensembl
-     *           ...TTC synonymous    source: dbSNP
-     *           ......CAC,CGC -> H,R source: COSMIC
-     *                 (one variant with two alleles)
-     */
-    SequenceI peptide = new Sequence("pep/10-12", "KFP");
-
-    /*
-     * two distinct variants for codon 1 position 1
-     * second one has clinical significance
-     */
-    String ensembl = "Ensembl";
-    String dbSnp = "dbSNP";
-    String cosmic = "COSMIC";
-
-    /*
-     * 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,
-            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,
-            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,
-            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,
-            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,
-            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,
-            0f, dbSnp);
-    sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous
-    sf6.setValue("id", "var6");
-
-    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8,
-            0f, cosmic);
-    sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R
-    sf7.setValue("id", "var7");
-    sf7.setValue("clinical_significance", "Good");
-
-    List<DnaVariant> codon1Variants = new ArrayList<>();
-    List<DnaVariant> codon2Variants = new ArrayList<>();
-    List<DnaVariant> codon3Variants = new ArrayList<>();
-
-    List<DnaVariant> codonVariants[] = new ArrayList[3];
-    codonVariants[0] = codon1Variants;
-    codonVariants[1] = codon2Variants;
-    codonVariants[2] = codon3Variants;
-
-    /*
-     * compute variants for protein position 1
-     */
-    codon1Variants.add(new DnaVariant("A", sf1));
-    codon1Variants.add(new DnaVariant("A", sf2));
-    codon1Variants.add(new DnaVariant("A", sf3));
-    codon2Variants.add(new DnaVariant("A"));
-    // codon2Variants.add(new DnaVariant("A"));
-    codon3Variants.add(new DnaVariant("A", sf4));
-    codon3Variants.add(new DnaVariant("A", sf5));
-    AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants);
-
-    /*
-     * compute variants for protein position 2
-     */
-    codon1Variants.clear();
-    codon2Variants.clear();
-    codon3Variants.clear();
-    codon1Variants.add(new DnaVariant("T"));
-    codon2Variants.add(new DnaVariant("T"));
-    codon3Variants.add(new DnaVariant("T", sf6));
-    AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants);
-
-    /*
-     * compute variants for protein position 3
-     */
-    codon1Variants.clear();
-    codon2Variants.clear();
-    codon3Variants.clear();
-    codon1Variants.add(new DnaVariant("C"));
-    codon2Variants.add(new DnaVariant("C", sf7));
-    codon3Variants.add(new DnaVariant("C"));
-    AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants);
-
-    /*
-     * verify added sequence features for
-     * var1 K -> E Ensembl
-     * var2 K -> Q dbSNP
-     * var3 K -> stop
-     * var4 synonymous
-     * var5 K -> N Ensembl
-     * var6 synonymous
-     * var7 P -> H COSMIC
-     * var8 P -> R COSMIC
-     */
-    List<SequenceFeature> sfs = peptide.getSequenceFeatures();
-    SequenceFeatures.sortFeatures(sfs, true);
-    assertEquals(8, sfs.size());
-
-    /*
-     * features are sorted by start position ascending, but in no
-     * particular order where start positions match; asserts here
-     * simply match the data returned (the order is not important)
-     */
-    // AAA -> AAT -> K/N
-    SequenceFeature sf = sfs.get(0);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
-    assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Asn", 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",
-            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("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Gln", 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",
-            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("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Glu", 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",
-            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("stop_gained", sf.getType());
-    assertEquals("Aaa/Taa", sf.getDescription());
-    assertEquals("var3", sf.getValue("id"));
-    assertEquals("Bad", sf.getValue("clinical_significance"));
-    assertEquals("id=var3;clinical_significance=Bad", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "Aaa/Taa var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3",
-            sf.links.get(0));
-    assertEquals(dbSnp, sf.getFeatureGroup());
-
-    // AAA -> AAG synonymous
-    sf = sfs.get(4);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
-    assertEquals("synonymous_variant", sf.getType());
-    assertEquals("aaA/aaG", sf.getDescription());
-    assertEquals("var4", sf.getValue("id"));
-    assertEquals("None", sf.getValue("clinical_significance"));
-    assertEquals("id=var4;clinical_significance=None", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "aaA/aaG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
-            sf.links.get(0));
-    assertEquals(cosmic, sf.getFeatureGroup());
-
-    // TTT -> TTC synonymous
-    sf = sfs.get(5);
-    assertEquals(2, sf.getBegin());
-    assertEquals(2, sf.getEnd());
-    assertEquals("synonymous_variant", sf.getType());
-    assertEquals("ttT/ttC", sf.getDescription());
-    assertEquals("var6", sf.getValue("id"));
-    assertNull(sf.getValue("clinical_significance"));
-    assertEquals("id=var6", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "ttT/ttC var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
-            sf.links.get(0));
-    assertEquals(dbSnp, sf.getFeatureGroup());
-
-    // 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("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Pro3Arg", 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",
-            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("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Pro3His", 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",
-            sf.links.get(0));
-    assertEquals(cosmic, sf.getFeatureGroup());
-  }
-
-  /**
    * Tests for the method that maps the subset of a dna sequence that has CDS
    * (or subtype) feature, with CDS strand = '-' (reverse)
    */
@@ -2592,9 +2190,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
@@ -2604,8 +2209,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
@@ -2623,15 +2228,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 ?
@@ -2643,10 +2248,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..de4ce6c
--- /dev/null
@@ -0,0 +1,120 @@
+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(groups = "Functional")
+  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 
+     * _and_ it contains "p." following a colon
+     */
+    Map<String, String> csq = new HashMap<>();
+    csq.put("HGVSp", "hello:world");
+    sf2.setValue("CSQ", csq);
+    variant = mf.findProteinVariants(sf2);
+    assertEquals(variant, "c.13T>C(p.=)");
+    csq.put("HGVSp", "p.HelloWorld");
+    variant = mf.findProteinVariants(sf2);
+    assertEquals(variant, "c.13T>C(p.=)");
+    csq.put("HGVSp", "try this:hellop.world");
+    variant = mf.findProteinVariants(sf2);
+    assertEquals(variant, "hellop.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 e42c34c..fc60949 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())
     {
@@ -541,7 +543,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());
@@ -560,10 +561,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 8fcf8f5..7b0da40 100644 (file)
@@ -1,6 +1,10 @@
 package jalview.io.vcf;
 
+import static jalview.io.gff.SequenceOntologyI.SEQUENCE_VARIANT;
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
 
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
@@ -9,12 +13,12 @@ import jalview.datamodel.Mapping;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.io.gff.Gff3Helper;
-import jalview.io.gff.SequenceOntologyI;
 import jalview.util.MapList;
 
 import java.io.File;
@@ -24,6 +28,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
 
 public class VCFLoaderTest
@@ -56,15 +61,21 @@ public class VCFLoaderTest
           + ">transcript4/1-18\n-----TGG-GGACGAGAGTGTGA-A\n";
 
   private static final String[] VCF = { "##fileformat=VCFv4.2",
+      // fields other than AF are ignored when parsing as they have no INFO definition
       "##INFO=<ID=AF,Number=A,Type=Float,Description=\"Allele Frequency, for each ALT allele, in the same order as listed\">",
+      "##INFO=<ID=AC_Female,Number=A,Type=Integer,Description=\"Allele count in Female genotypes\"",
+      "##INFO=<ID=AF_AFR,Number=A,Type=Float,Description=\"Allele Frequency among African/African American genotypes\"",
       "##reference=Homo_sapiens/GRCh38",
       "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
       // A/T,C variants in position 2 of gene sequence (precedes transcript)
-      // should create 2 variant features with respective scores
-      "17\t45051611\t.\tA\tT,C\t1666.64\tRF\tAC=15;AF=5.0e-03,4.0e-03",
+      // should create 2 variant features with respective AF values
+      // malformed values for AC_Female and AF_AFR should be ignored
+      "17\t45051611\trs384765\tA\tT,C\t1666.64\tRF;XYZ\tAC=15;AF=5.0e-03,4.0e-03;AC_Female=12,3d;AF_AFR=low,2.3e-4",
       // SNP G/C in position 4 of gene sequence, position 2 of transcript
       // insertion G/GA is transferred to nucleotide but not to peptide
-      "17\t45051613\t.\tG\tGA,C\t1666.64\tRF\tAC=15;AF=3.0e-03,2.0e-03" };
+      "17\t45051613\t.\tG\tGA,C\t1666.65\t.\tAC=15;AF=3.0e-03,2.0e-03",
+      // '.' in INFO field should be ignored
+      "17\t45051615\t.\tG\tC\t1666.66\tRF\tAC=16;AF=." };
 
   @BeforeClass(alwaysRun = true)
   public void setUp()
@@ -79,12 +90,21 @@ public class VCFLoaderTest
     Cache.initLogger();
   }
 
+  @BeforeTest(alwaysRun = true)
+  public void setUpBeforeTest()
+  {
+    /*
+     * clear down feature attributes metadata
+     */
+    FeatureAttributes.getInstance().clear();
+  }
+
   @Test(groups = "Functional")
   public void testDoLoad() throws IOException
   {
     AlignmentI al = buildAlignment();
 
-    File f = makeVcf();
+    File f = makeVcfFile();
     VCFLoader loader = new VCFLoader(f.getPath());
 
     loader.doLoad(al.getSequencesArray(), null);
@@ -99,31 +119,41 @@ public class VCFLoaderTest
     List<SequenceFeature> geneFeatures = al.getSequenceAt(0)
             .getSequenceFeatures();
     SequenceFeatures.sortFeatures(geneFeatures, true);
-    assertEquals(geneFeatures.size(), 4);
+    assertEquals(geneFeatures.size(), 5);
     SequenceFeature sf = geneFeatures.get(0);
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 2);
     assertEquals(sf.getEnd(), 2);
     assertEquals(sf.getScore(), 0f);
-    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 4.0e-03,
-            DELTA);
+    assertEquals(sf.getValue("AF"), "4.0e-03");
+    assertEquals(sf.getValue("AF_AFR"), "2.3e-4");
     assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,C");
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
+    assertEquals(sf.getValue("POS"), "45051611");
+    assertEquals(sf.getValue("ID"), "rs384765");
+    assertEquals(sf.getValue("QUAL"), "1666.64");
+    assertEquals(sf.getValue("FILTER"), "RF;XYZ");
+    // malformed integer for AC_Female is ignored (JAL-3375)
+    assertNull(sf.getValue("AC_Female"));
+
     sf = geneFeatures.get(1);
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 2);
     assertEquals(sf.getEnd(), 2);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 5.0e-03,
             DELTA);
+    assertEquals(sf.getValue("AC_Female"), "12");
+    // malformed float for AF_AFR is ignored (JAL-3375)
+    assertNull(sf.getValue("AC_AFR"));
     assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,T");
 
     sf = geneFeatures.get(2);
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 4);
     assertEquals(sf.getEnd(), 4);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
             DELTA);
@@ -133,23 +163,35 @@ public class VCFLoaderTest
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 4);
     assertEquals(sf.getEnd(), 4);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
             DELTA);
     assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
+    assertNull(sf.getValue("ID")); // '.' is ignored
+    assertNull(sf.getValue("FILTER")); // '.' is ignored
+
+    sf = geneFeatures.get(4);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 6);
+    assertEquals(sf.getEnd(), 6);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 0f);
+    // AF=. should not have been captured
+    assertNull(sf.getValue("AF"));
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
 
     /*
      * verify variant feature(s) added to transcript
      */
     List<SequenceFeature> transcriptFeatures = al.getSequenceAt(1)
             .getSequenceFeatures();
-    assertEquals(transcriptFeatures.size(), 2);
+    assertEquals(transcriptFeatures.size(), 3);
     sf = transcriptFeatures.get(0);
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 2);
     assertEquals(sf.getEnd(), 2);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
             DELTA);
@@ -158,7 +200,7 @@ public class VCFLoaderTest
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 2);
     assertEquals(sf.getEnd(), 2);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
             DELTA);
@@ -178,16 +220,14 @@ 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());
   }
 
-  private File makeVcf() throws IOException
+  private File makeVcfFile() throws IOException
   {
     File f = File.createTempFile("Test", ".vcf");
     f.deleteOnExit();
@@ -327,7 +367,7 @@ public class VCFLoaderTest
   {
     AlignmentI al = buildAlignment();
 
-    File f = makeVcf();
+    File f = makeVcfFile();
 
     VCFLoader loader = new VCFLoader(f.getPath());
 
@@ -340,96 +380,97 @@ public class VCFLoaderTest
     List<SequenceFeature> geneFeatures = al.getSequenceAt(2)
             .getSequenceFeatures();
     SequenceFeatures.sortFeatures(geneFeatures, true);
-    assertEquals(geneFeatures.size(), 4);
+    assertEquals(geneFeatures.size(), 5);
+    SequenceFeature sf;
 
     /*
-     * variant A/T at 45051611 maps to T/A at gene position 24
+     * insertion G/GA at 45051613 maps to an insertion at
+     * the preceding position (21) on reverse strand gene
+     * reference: CAAGC -> GCTTG/21-25
+     * genomic variant: CAAGAC (G/GA)
+     * gene variant: GTCTTG (G/GT at 21)
      */
-    SequenceFeature sf = geneFeatures.get(3);
+    sf = geneFeatures.get(1);
     assertEquals(sf.getFeatureGroup(), "VCF");
-    assertEquals(sf.getBegin(), 24);
-    assertEquals(sf.getEnd(), 24);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getBegin(), 21);
+    assertEquals(sf.getEnd(), 21);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
-    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 5.0e-03,
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
+    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
             DELTA);
-    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,A");
 
     /*
-     * variant A/C at 45051611 maps to T/G at gene position 24
+     * variant G/C at 45051613 maps to C/G at gene position 22
      */
     sf = geneFeatures.get(2);
     assertEquals(sf.getFeatureGroup(), "VCF");
-    assertEquals(sf.getBegin(), 24);
-    assertEquals(sf.getEnd(), 24);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getBegin(), 22);
+    assertEquals(sf.getEnd(), 22);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
-    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 4.0e-03,
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
+    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
             DELTA);
-    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,G");
 
     /*
-     * variant G/C at 45051613 maps to C/G at gene position 22
+     * variant A/C at 45051611 maps to T/G at gene position 24
      */
-    sf = geneFeatures.get(1);
+    sf = geneFeatures.get(3);
     assertEquals(sf.getFeatureGroup(), "VCF");
-    assertEquals(sf.getBegin(), 22);
-    assertEquals(sf.getEnd(), 22);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getBegin(), 24);
+    assertEquals(sf.getEnd(), 24);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
-    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,G");
+    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 4.0e-03,
             DELTA);
-    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
 
     /*
-     * insertion G/GA at 45051613 maps to an insertion at
-     * the preceding position (21) on reverse strand gene
-     * reference: CAAGC -> GCTTG/21-25
-     * genomic variant: CAAGAC (G/GA)
-     * gene variant: GTCTTG (G/GT at 21)
+     * variant A/T at 45051611 maps to T/A at gene position 24
      */
-    sf = geneFeatures.get(0);
+    sf = geneFeatures.get(4);
     assertEquals(sf.getFeatureGroup(), "VCF");
-    assertEquals(sf.getBegin(), 21);
-    assertEquals(sf.getEnd(), 21);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getBegin(), 24);
+    assertEquals(sf.getEnd(), 24);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
-    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,A");
+    assertEquals(Float.parseFloat((String) sf.getValue("AF")), 5.0e-03,
             DELTA);
-    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
 
     /*
-     * verify 2 variant features added to transcript2
+     * verify 3 variant features added to transcript2
      */
     List<SequenceFeature> transcriptFeatures = al.getSequenceAt(3)
             .getSequenceFeatures();
-    assertEquals(transcriptFeatures.size(), 2);
+    assertEquals(transcriptFeatures.size(), 3);
 
     /*
      * insertion G/GT at position 21 of gene maps to position 16 of transcript
      */
-    sf = transcriptFeatures.get(0);
+    sf = transcriptFeatures.get(1);
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 16);
     assertEquals(sf.getEnd(), 16);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
             DELTA);
-    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
 
     /*
      * SNP C/G at position 22 of gene maps to position 17 of transcript
      */
-    sf = transcriptFeatures.get(1);
+    sf = transcriptFeatures.get(2);
     assertEquals(sf.getFeatureGroup(), "VCF");
     assertEquals(sf.getBegin(), 17);
     assertEquals(sf.getEnd(), 17);
-    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getType(), SEQUENCE_VARIANT);
     assertEquals(sf.getScore(), 0f);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
             DELTA);
-    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
 
     /*
      * verify variant feature(s) computed and added to protein
@@ -445,13 +486,11 @@ 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());
   }
 
   /**
@@ -494,6 +533,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 +543,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 +646,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
@@ -717,4 +762,16 @@ public class VCFLoaderTest
     assertEquals(sf.getEnd(), 15);
     assertEquals(sf.getDescription(), "T,C");
   }
+
+  @Test(groups = "Functional")
+  public void testDecodeSpecialCharacters() throws IOException
+  {
+    String encoded = "hello world";
+    String decoded = VCFLoader.decodeSpecialCharacters(encoded);
+    assertSame(encoded, decoded); // no change needed
+
+    encoded = "ab%3Acd%3Bef%3Dgh%25ij%2Ckl%3A";
+    decoded = VCFLoader.decodeSpecialCharacters(encoded);
+    assertEquals(decoded, "ab:cd;ef=gh%ij,kl:");
+  }
 }
\ No newline at end of file
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 83cc683..8188a8d 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;
@@ -845,13 +845,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);
@@ -927,7 +930,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()