Merge branch 'develop' into feature/JAL-3390hideUnmappedStructure
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 8 Jun 2020 15:14:08 +0000 (16:14 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 8 Jun 2020 15:14:08 +0000 (16:14 +0100)
Conflicts:
gradle.properties
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/api/structures/JalviewStructureDisplayI.java
src/jalview/appletgui/AppletJmol.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/ext/rbvi/chimera/AtomSpecModel.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AppJmol.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/StructureViewerBase.java
src/jalview/structures/models/AAStructureBindingModel.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/structures/models/AAStructureBindingModelTest.java

30 files changed:
gradle.properties
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/api/AlignmentViewPanel.java
src/jalview/appletgui/AlignmentPanel.java
src/jalview/appletgui/AppletJmol.java
src/jalview/appletgui/AppletJmolBinding.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/ext/rbvi/chimera/AtomSpecModel.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AppJmol.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/ViewSelectionMenu.java
src/jalview/jbgui/GStructureViewer.java
src/jalview/structure/StructureCommandsBase.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/structures/models/AAStructureBindingModel.java
src/jalview/util/StructureCommands.java [new file with mode: 0644]
test/jalview/ext/jmol/JalviewJmolBindingTest.java [new file with mode: 0644]
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/JalviewChimeraBindingTest.java [new file with mode: 0644]
test/jalview/structures/models/AAStructureBindingModelTest.java
utils/testnglibs/testng-sources.jar

index b99cdca..3781991 100644 (file)
@@ -3,7 +3,6 @@
 #
 
 jalviewDir = .
-
 #JAVA_VERSION = 1.8
 JAVA_VERSION = 11
 
index ca6ae63..afba050 100644 (file)
@@ -1404,6 +1404,8 @@ 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_alignment_only = Show alignment only
+label.hide_hidden_regions = Hide hidden columns / sequences
 label.show_linked_features = Show {0} features
 label.on_top = on top
 label.include_linked_features = Include {0} features
index 624e619..98274c6 100644 (file)
@@ -311,7 +311,8 @@ label.copied_sequences_to_clipboard = Copiadas {0} secuencias en el portapapeles
 label.check_file_matches_sequence_ids_alignment = Comprobar que el fichero coincide con el ID de la secuencia en el alineamiento.
 label.problem_reading_tcoffee_score_file = Problema de lectura del fichero de puntuaciones T-COFFEE
 label.source_to_target = {0} a {1}
-label.per_sequence_only= Sólo por secuencia
+label.per_sequence_
+= Sólo por secuencia
 label.to_file = a fichero
 label.to_textbox = a cuadro de texto
 label.jalview = Jalview
@@ -1400,6 +1401,8 @@ 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_alignment_only = Mostrar solo alineamiento
+label.hide_hidden_regions = Ocultar columnas / secuencias ocultas
 label.show_linked_features = Características de {0}
 label.on_top = encima
 label.include_linked_features = Incluir características de {0}
index 0b1ca21..ad3fbed 100644 (file)
@@ -63,4 +63,18 @@ public interface AlignmentViewPanel extends OOMHandlerI
    * @return displayed name for the view
    */
   String getViewName();
+
+  /**
+   * Answers the title of the panel
+   * 
+   * @return
+   */
+  String getTitle();
+
+  /**
+   * Make/Unmake this alignment panel the current input focus
+   * 
+   * @param b
+   */
+  void setSelected(boolean b);
 }
index 58569cd..1b1d902 100644 (file)
@@ -1158,4 +1158,16 @@ public class AlignmentPanel extends Panel
 
   }
 
+  @Override
+  public String getTitle()
+  {
+    return alignFrame.getTitle();
+  }
+
+  @Override
+  public void setSelected(boolean b)
+  {
+    // noop
+  }
+
 }
index 6665ec8..f73ff88 100644 (file)
@@ -394,19 +394,20 @@ public class AppletJmol extends EmbmenuFrame implements
 
   void centerViewer()
   {
-    Vector<String> toshow = new Vector<>();
+    Vector<String> toHide = new Vector<>();
     for (int i = 0; i < chainMenu.getItemCount(); i++)
     {
       if (chainMenu.getItem(i) instanceof CheckboxMenuItem)
       {
         CheckboxMenuItem item = (CheckboxMenuItem) chainMenu.getItem(i);
-        if (item.getState())
+        if (!item.getState())
         {
-          toshow.addElement(item.getLabel());
+          toHide.addElement(item.getLabel());
         }
       }
     }
-    jmb.showChains(toshow);
+    jmb.setChainsToHide(toHide);
+    jmb.centerViewer();
   }
 
   void closeViewer()
@@ -548,7 +549,7 @@ public class AppletJmol extends EmbmenuFrame implements
     else if (evt.getSource() == seqColour)
     {
       setEnabled(seqColour);
-      jmb.colourBySequence(ap);
+      jmb.updateStructureColours(ap);
     }
     else if (!allChainsSelected)
     {
@@ -581,7 +582,7 @@ public class AppletJmol extends EmbmenuFrame implements
   public void updateColours(Object source)
   {
     AlignmentPanel panel = (AlignmentPanel) source;
-    jmb.colourBySequence(panel);
+    jmb.updateStructureColours(panel);
   }
 
   public void updateTitleAndMenus()
@@ -592,7 +593,7 @@ public class AppletJmol extends EmbmenuFrame implements
       return;
     }
     setChainMenuItems(jmb.getChainNames());
-    jmb.colourBySequence(ap);
+    jmb.updateStructureColours(ap);
 
     setTitle(jmb.getViewerTitle());
   }
index c7ce994..9639f1e 100644 (file)
@@ -95,7 +95,7 @@ class AppletJmolBinding extends JalviewJmolBinding
   public void updateColours(Object source)
   {
     AlignmentPanel ap = (AlignmentPanel) source;
-    colourBySequence(ap);
+    updateStructureColours(ap);
   }
 
   @Override
index eee48df..09f3876 100644 (file)
@@ -39,6 +39,7 @@ import org.jmol.api.JmolViewer;
 import org.jmol.c.CBK;
 import org.jmol.viewer.Viewer;
 
+import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
@@ -50,6 +51,7 @@ import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
 import jalview.structure.AtomSpec;
+import jalview.structure.AtomSpecModel;
 import jalview.structure.StructureCommand;
 import jalview.structure.StructureCommandI;
 import jalview.structure.StructureSelectionManager;
@@ -115,12 +117,43 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     return getViewerTitle("Jmol", true);
   }
 
+  /**
+   * prepare the view for a given set of models/chains. chainList contains strings
+   * of the form 'pdbfilename:Chaincode'
+   * 
+   * @deprecated now only used by applet code
+   */
+  @Deprecated
+  public void centerViewer()
+  {
+    StringBuilder cmd = new StringBuilder(128);
+    int mlength, p;
+    for (String lbl : chainsToHide)
+    {
+      mlength = 0;
+      do
+      {
+        p = mlength;
+        mlength = lbl.indexOf(":", p);
+      } while (p < mlength && mlength < (lbl.length() - 2));
+      // TODO: lookup each pdb id and recover proper model number for it.
+      cmd.append(":" + lbl.substring(mlength + 1) + " /"
+              + (getModelIdForFile(getFileForChain(lbl))) + " or ");
+    }
+    if (cmd.length() > 0)
+    {
+      cmd.setLength(cmd.length() - 4);
+    }
+    // todo: revised command is untested - but this method is obsolete anyway
+    String command = "select *;hide " + cmd + ";cartoon;center " + cmd;
+    executeCommand(new StructureCommand(command), false);
+  }
+
   private String jmolScript(String script)
   {
     Cache.log.debug(">>Jmol>> " + script);
     String s = jmolViewer.evalStringQuiet(script); // scriptWait(script); BH
     Cache.log.debug("<<Jmol<< " + s);
-
     return s;
   }
 
@@ -191,34 +224,36 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   @Override
   public synchronized String[] getStructureFiles()
   {
+    if (modelFileNames != null)
+    {
+      return modelFileNames;
+    }
     if (jmolViewer == null)
     {
       return new String[0];
     }
 
-    if (modelFileNames == null)
+    List<String> mset = new ArrayList<>();
+    int modelCount = jmolViewer.ms.mc;
+    String filePath = null;
+    for (int i = 0; i < modelCount; ++i)
     {
-      int modelCount = jmolViewer.ms.mc;
-      String filePath = null;
-      List<String> mset = new ArrayList<>();
-      for (int i = 0; i < modelCount; ++i)
+      /*
+       * defensive check for null as getModelFileName can return null
+       * even when model count ms.mc is > 0
+       */
+      filePath = jmolViewer.ms.getModelFileName(i);
+      if (filePath != null && !mset.contains(filePath))
       {
-        /*
-         * defensive check for null as getModelFileName can return null
-         * even when model count ms.mc is > 0
-         */
-        filePath = jmolViewer.ms.getModelFileName(i);
-        if (filePath != null && !mset.contains(filePath))
-        {
-          mset.add(filePath);
-        }
-      }
-      if (!mset.isEmpty())
-      {
-        modelFileNames = mset.toArray(new String[mset.size()]);
+        mset.add(filePath);
       }
     }
 
+    if (!mset.isEmpty())
+    {
+      modelFileNames = mset.toArray(new String[mset.size()]);
+    }
+
     return modelFileNames;
   }
 
@@ -966,6 +1001,85 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   }
 
   @Override
+  public void showStructures(AlignViewportI av, boolean refocus)
+  {
+    String cmd = buildShowStructuresCommand(av, refocus);
+    executeCommand(new StructureCommand(cmd), false);
+  }
+
+  /**
+   * Builds a command to show parts of the structure, depending on whether
+   * <ul>
+   * <li>all structures or regions mapped to alignment only are shown</li>
+   * <li>all chains or only selected chains are shown</li>
+   * </ul>
+   * 
+   * @param av
+   * @param refocus
+   * @return
+   */
+  protected String buildShowStructuresCommand(AlignViewportI av,
+          boolean refocus)
+  {
+    StringBuilder cmd = new StringBuilder(128);
+    if (!isShowAlignmentOnly())
+    {
+      cmd.append("display *");
+    }
+    else
+    {
+      AtomSpecModel model = getShownResidues(av);
+      String atomSpec = getCommandGenerator().getAtomSpec(model, false);
+
+      cmd.append("hide *;display ").append(atomSpec)
+              .append("; select displayed");
+    }
+
+    /*
+     * hide any chains not selected to be shown
+     */
+    if (!chainsToHide.isEmpty())
+    {
+      cmd.append("; hide add ");
+      boolean firstHide = true;
+      for (String pdbChain : chainsToHide)
+      {
+        String[] toks = pdbChain.split(":");
+        String chainId = toks[1];
+        String modelNo = getModelIdForFile(getFileForChain(pdbChain));
+        if ("".equals(modelNo))
+        {
+          continue;
+        }
+        if (!firstHide)
+        {
+          cmd.append(",");
+        }
+        firstHide = false;
+        cmd.append(":").append(chainId).append("/")
+                .append(String.valueOf(modelNo)).append(".1");
+      }
+    }
+
+    cmd.append("; cartoon only");
+    if (refocus)
+    {
+      cmd.append("; zoom 0");
+    }
+    return cmd.toString();
+  }
+
+  /**
+   * Answers a Jmol syntax style structure model specification. Model number 0, 1,
+   * 2... is formatted as "1.1", "2.1", "3.1" etc.
+   */
+  @Override
+  public String getModelSpec(int model)
+  {
+    return String.valueOf(model + 1) + ".1";
+  }
+
+  @Override
   protected String getModelIdForFile(String pdbFile)
   {
     if (modelFileNames == null)
index 085fbd5..7dc5224 100644 (file)
@@ -51,6 +51,8 @@ import jalview.util.Platform;
  */
 public class JmolCommands extends StructureCommandsBase
 {
+  private static final String COMMA = ",";
+
   private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
           "select *; cartoons off; backbone");
 
@@ -74,11 +76,6 @@ public class JmolCommands extends StructureCommandsBase
 
   private static final String SLASH = "/";
 
-  /**
-   * {@inheritDoc}
-   * 
-   * @return
-   */
   @Override
   public int getModelStartNo()
   {
@@ -243,9 +240,13 @@ public class JmolCommands extends StructureCommandsBase
    * Generates a Jmol atomspec string like
    * 
    * <pre>
-   * 2-5:A/1.1,8:A/1.1,5-10:B/2.1
+   * (61-64,70)&:A/1.1,(12-25,41-44)&:B/1.1,12:A/2.1
+   * for model 1, chain A, residues 61-64 and 70, chain B residues 12-25 and 41-44, model 2 chain A residue 12
    * </pre>
    * 
+   * Note the brackets to group multiple residue ranges for the same chain
+   * (without bracketing, ranges would apply to all chains)
+   * 
    * Parameter {@code alphaOnly} is not used here - this restriction is made by
    * a separate clause in the {@code compare} (superposition) command.
    */
@@ -254,29 +255,42 @@ public class JmolCommands extends StructureCommandsBase
   {
     StringBuilder sb = new StringBuilder(128);
 
-    boolean first = true;
+    boolean firstChain = true;
     for (String modelNo : model.getModels())
     {
       for (String chain : model.getChains(modelNo))
       {
-        for (int[] range : model.getRanges(modelNo, chain))
+        if (!firstChain)
         {
-          if (!first)
-          {
-            sb.append(PIPE);
-          }
-          first = false;
-          if (range[0] == range[1])
+          sb.append(COMMA);
+        }
+        firstChain = false;
+        List<int[]> rangeList = model.getRanges(modelNo, chain);
+        if (rangeList.size() > 1)
+        {
+          sb.append("(");
+        }
+        boolean firstRange = true;
+        for (int[] range : rangeList)
+        {
+          if (!firstRange)
           {
-            sb.append(range[0]);
+            sb.append(COMMA);
           }
-          else
+          firstRange = false;
+          firstChain = false;
+          sb.append(range[0]);
+          if (range[0] != range[1])
           {
-            sb.append(range[0]).append(HYPHEN).append(range[1]);
+            sb.append(HYPHEN).append(range[1]);
           }
-          sb.append(COLON).append(chain.trim()).append(SLASH);
-          sb.append(String.valueOf(modelNo)).append(".1");
         }
+        if (rangeList.size() > 1)
+        {
+          sb.append(")&");
+        }
+        sb.append(COLON).append(chain.trim()).append(SLASH);
+        sb.append(String.valueOf(modelNo)).append(".1");
       }
     }
 
diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java
new file mode 100644 (file)
index 0000000..f0d1e84
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.ext.rbvi.chimera;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A class to model a Chimera or Jmol residue set, as
+ * {@code Map<modelNumber, Map<chainId, List<residueRange>>>}. This can then be
+ * traversed to generate the required display command in Chimera or Jmol syntax.
+ */
+public class AtomSpecModel
+{
+  private Map<Integer, Map<String, List<int[]>>> atomSpec;
+
+  /**
+   * Constructor
+   */
+  public AtomSpecModel()
+  {
+    atomSpec = new TreeMap<>();
+  }
+
+  public Map<Integer, Map<String, List<int[]>>> getMap()
+  {
+    return atomSpec;
+  }
+
+  /**
+   * Adds one contiguous range to this atom spec
+   * 
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public void addRange(int model, int startPos, int endPos, String chain)
+  {
+    /*
+     * Get/initialize map of data for the colour and model
+     */
+    Map<String, List<int[]>> modelData = atomSpec.get(model);
+    if (modelData == null)
+    {
+      atomSpec.put(model, modelData = new TreeMap<>());
+    }
+
+    /*
+     * Get/initialize map of data for colour, model and chain
+     */
+    List<int[]> chainData = modelData.get(chain);
+    if (chainData == null)
+    {
+      chainData = new ArrayList<>();
+      modelData.put(chain, chainData);
+    }
+
+    /*
+     * Add the start/end positions
+     */
+    chainData.add(new int[] { startPos, endPos });
+    // TODO add intelligently, using a RangeList class
+  }
+
+  /**
+   * Answers an iterable set of the structure models in this model
+   * 
+   * @return
+   */
+  public Iterable<Integer> getModels()
+  {
+    return atomSpec.keySet();
+  }
+
+  /**
+   * Answers an iterable set of the chains in this model for the given structure
+   * model, or an empty set if none
+   * 
+   * @param model
+   * @return
+   */
+  public Iterable<String> getChains(Integer model)
+  {
+    if (atomSpec.containsKey(model))
+    {
+      return atomSpec.get(model).keySet();
+    }
+    return Collections.emptySet();
+  }
+
+  public List<int[]> getRanges(Integer model, String chain)
+  {
+    Map<String, List<int[]>> modelData = atomSpec.get(model);
+    if (modelData != null)
+    {
+      List<int[]> chainData = modelData.get(chain);
+      if (chainData != null)
+      {
+        return chainData;
+      }
+    }
+    return Collections.EMPTY_LIST;
+  }
+}
index 5beee56..642035f 100644 (file)
@@ -84,6 +84,7 @@ public class ChimeraCommands extends StructureCommandsBase
    * </pre>
    * 
    * @param featureMap
+   * @param binding
    * @return
    */
   @Override
index 460b156..0907695 100644 (file)
@@ -36,6 +36,7 @@ import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
+import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
@@ -271,7 +272,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * @param pdbfnum
    * @return
    */
-  protected String getModelSpec(int pdbfnum)
+  @Override
+  public String getModelSpec(int pdbfnum)
   {
     if (pdbfnum < 0 || pdbfnum >= getPdbCount())
     {
@@ -284,9 +286,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
      * to the Chimera command 'list models type molecule', see
      * ChimeraManager.getModelList().
      */
-    List<ChimeraModel> maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
+    String[] structureFiles = getStructureFiles();
+    if (pdbfnum < 0 || pdbfnum >= structureFiles.length)
+    {
+      return "";
+    }
+
+    List<ChimeraModel> maps = chimeraMaps.get(structureFiles[pdbfnum]);
     boolean hasSubModels = maps != null && maps.size() > 1;
-    return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
+    String spec = "#" + String.valueOf(pdbfnum);
+    return hasSubModels ? spec + ".1" : spec;
   }
 
   /**
@@ -577,7 +586,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   public int sendFeaturesToViewer(AlignmentViewPanel avp)
   {
     // TODO refactor as required to pull up to an interface
-
     Map<String, Map<Object, AtomSpecModel>> featureValues = buildFeaturesMap(
             avp);
     List<StructureCommandI> commands = getCommandGenerator()
@@ -822,4 +830,80 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   {
     return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide";
   }
+
+  @Override
+  public void showStructures(AlignViewportI av, boolean refocus)
+  {
+    StructureCommandI cmd = buildShowStructuresCommand(av, refocus);
+    executeCommand(cmd, false);
+  }
+
+  /**
+   * Builds a command to show parts of the structure, depending on whether
+   * <ul>
+   * <li>all structures or regions mapped to alignment only are shown</li>
+   * <li>all chains or only selected chains are shown</li>
+   * </ul>
+   * 
+   * @param av
+   * @param refocus
+   * @return
+   */
+  protected StructureCommandI buildShowStructuresCommand(
+          AlignViewportI av,
+          boolean refocus)
+  {
+    // TODO refactor using command generator
+    // pull up this method and Jmol variant to base class
+    StringBuilder cmd = new StringBuilder(128);
+    cmd.append("~display");
+
+    if (isShowAlignmentOnly())
+    {
+      AtomSpecModel model = getShownResidues(av);
+      String atomSpec = getCommandGenerator().getAtomSpec(model, false);
+      if (!atomSpec.isEmpty())
+      {
+        cmd.append("; ~ribbon; ribbon ").append(atomSpec);
+      }
+    }
+    else
+    {
+      cmd.append("; ribbon");
+    }
+
+    /*
+     * hide any chains selected not to be shown (whether mapped to
+     * sequence in the alignment or not)
+     */
+    for (String pdbChain : chainsToHide)
+    {
+      String chainId = pdbChain.split(":")[1];
+      String modelNo = getModelIdForFile(getFileForChain(pdbChain));
+      if (!"".equals(modelNo))
+      {
+        cmd.append("; ~ribbon #").append(modelNo).append(":.")
+                .append(chainId);
+      }
+    }
+    if (refocus)
+    {
+      cmd.append("; focus");
+    }
+    return new StructureCommand(cmd.toString());
+  }
+
+  @Override
+  public int getModelForPdbFile(String fileName, int fileIndex)
+  {
+    if (chimeraMaps.containsKey(fileName))
+    {
+      List<ChimeraModel> models = chimeraMaps.get(fileName);
+      if (!models.isEmpty())
+      {
+        return models.get(0).getModelNumber();
+      }
+    }
+    return -1;
+  }
 }
index ef40261..2ca02fd 100644 (file)
@@ -423,7 +423,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             {
 
               @Override
-              public AlignmentPanel[] getAllAlignmentPanels()
+              public AlignmentViewPanel[] getAllAlignmentPanels()
               {
                 origview.clear();
                 origview.add(alignPanel);
@@ -441,7 +441,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
               {
                 if (origview.size() > 0)
                 {
-                  final AlignmentPanel ap = origview.get(0);
+                  final AlignmentViewPanel avp = origview.get(0);
 
                   /*
                    * Copy the ViewStyle of the selected panel to 'this one'.
@@ -454,10 +454,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                           .getAlignViewport().getCodingComplement() != null;
                   if (!fromSplitFrame)
                   {
-                    vs.setScaleProteinAsCdna(ap.getAlignViewport()
+                    vs.setScaleProteinAsCdna(avp.getAlignViewport()
                             .getViewStyle().isScaleProteinAsCdna());
                   }
-                  ap.getAlignViewport().setViewStyle(vs);
+                  avp.getAlignViewport().setViewStyle(vs);
 
                   /*
                    * Also rescale ViewStyle of SplitFrame complement if there is
@@ -465,7 +465,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                    * the whole ViewStyle (allow cDNA protein to have different
                    * fonts)
                    */
-                  AlignViewportI complement = ap.getAlignViewport()
+                  AlignViewportI complement = avp.getAlignViewport()
                           .getCodingComplement();
                   if (complement != null && vs.isScaleProteinAsCdna())
                   {
@@ -475,10 +475,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                     af.setMenusForViewport();
                   }
 
+                  avp.setSelected(true);
+                  AlignmentPanel ap = (AlignmentPanel) avp;
                   ap.updateLayout();
-                  ap.setSelected(true);
                   ap.alignFrame.setMenusForViewport();
-
                 }
               }
             });
index 45e4b95..7ee0d80 100644 (file)
  */
 package jalview.gui;
 
-import jalview.analysis.AnnotationSorter;
-import jalview.api.AlignViewportI;
-import jalview.api.AlignmentViewPanel;
-import jalview.bin.Cache;
-import jalview.bin.Jalview;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
-import jalview.datamodel.SearchResultsI;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceGroup;
-import jalview.datamodel.SequenceI;
-import jalview.gui.ImageExporter.ImageWriterI;
-import jalview.io.HTMLOutput;
-import jalview.jbgui.GAlignmentPanel;
-import jalview.math.AlignmentDimension;
-import jalview.schemes.ResidueProperties;
-import jalview.structure.StructureSelectionManager;
-import jalview.util.Comparison;
-import jalview.util.ImageMaker;
-import jalview.util.MessageManager;
-import jalview.viewmodel.ViewportListenerI;
-import jalview.viewmodel.ViewportRanges;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Container;
@@ -67,6 +44,29 @@ import java.util.List;
 
 import javax.swing.SwingUtilities;
 
+import jalview.analysis.AnnotationSorter;
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
+import jalview.bin.Jalview;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.SearchResultsI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.gui.ImageExporter.ImageWriterI;
+import jalview.io.HTMLOutput;
+import jalview.jbgui.GAlignmentPanel;
+import jalview.math.AlignmentDimension;
+import jalview.schemes.ResidueProperties;
+import jalview.structure.StructureSelectionManager;
+import jalview.util.Comparison;
+import jalview.util.ImageMaker;
+import jalview.util.MessageManager;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
+
 /**
  * DOCUMENT ME!
  * 
@@ -1526,6 +1526,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
    * 
    * @param b
    */
+  @Override
   public void setSelected(boolean b)
   {
     try
@@ -1737,4 +1738,9 @@ public class AlignmentPanel extends GAlignmentPanel implements
     return calculationDialog;
   }
 
+  @Override
+  public String getTitle()
+  {
+    return alignFrame == null ? null : alignFrame.getTitle();
+  }
 }
index 7cf10e7..06ce7ca 100644 (file)
@@ -374,9 +374,9 @@ public class AppJmol extends StructureViewerBase
     }
 
     // refresh the sequence colours for the new structure(s)
-    for (AlignmentViewPanel ap : _colourwith)
+    for (AlignmentViewPanel avp : _colourwith)
     {
-      jmb.updateColours(ap);
+      jmb.updateColours(avp);
     }
     // do superposition if asked to
     if (alignAddedStructures)
index 0e5675c..55fe9da 100644 (file)
@@ -82,7 +82,6 @@ public class ChimeraViewFrame extends StructureViewerBase
     super.initMenus();
 
     savemenu.setVisible(false); // not yet implemented
-    viewMenu.add(fitToWindow);
 
     JMenuItem writeFeatures = new JMenuItem(
             MessageManager.getString("label.create_viewer_attributes"));
@@ -507,9 +506,9 @@ public class ChimeraViewFrame extends StructureViewerBase
       }
 
       // refresh the sequence colours for the new structure(s)
-      for (AlignmentViewPanel ap : _colourwith)
+      for (AlignmentViewPanel avp : _colourwith)
       {
-        jmb.updateColours(ap);
+        jmb.updateColours(avp);
       }
       // do superposition if asked to
       if (alignAddedStructures)
@@ -543,12 +542,6 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   @Override
-  protected void fitToWindow_actionPerformed()
-  {
-    jmb.focusView();
-  }
-
-  @Override
   public ViewerType getViewerType()
   {
     return ViewerType.CHIMERA;
index 49655a4..c66fdfc 100644 (file)
@@ -20,6 +20,8 @@
  */
 package jalview.gui;
 
+import javax.swing.JComponent;
+
 import jalview.api.AlignmentViewPanel;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.datamodel.PDBEntry;
@@ -28,9 +30,6 @@ import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 import jalview.io.DataSourceType;
 import jalview.structure.StructureSelectionManager;
 
-import javax.swing.JComponent;
-import javax.swing.SwingUtilities;
-
 public class JalviewChimeraBindingModel extends JalviewChimeraBinding
 {
   public JalviewChimeraBindingModel(ChimeraViewFrame chimeraViewFrame,
index 33a122c..900252a 100644 (file)
@@ -33,6 +33,7 @@ import java.io.FileReader;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Random;
 import java.util.Vector;
@@ -75,6 +76,8 @@ import jalview.ws.dbsources.Pdb;
 public abstract class StructureViewerBase extends GStructureViewer
         implements Runnable, ViewSetProvider
 {
+  private static final String UNMAPPED = "(unmapped)";
+
   /*
    * names for colour options (additional to Jalview colour schemes)
    */
@@ -171,9 +174,9 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   @Override
-  public boolean isUsedForColourBy(AlignmentViewPanel ap2)
+  public boolean isUsedForColourBy(AlignmentViewPanel avp)
   {
-    return (_colourwith != null) && _colourwith.contains(ap2);
+    return (_colourwith != null) && _colourwith.contains(avp);
   }
 
   /**
@@ -517,6 +520,10 @@ public abstract class StructureViewerBase extends GStructureViewer
     {
       return;
     }
+
+    /*
+     * add the 'All' menu item
+     */
     JMenuItem menuItem = new JMenuItem(
             MessageManager.getString("label.all"));
     menuItem.addActionListener(new ActionListener()
@@ -536,12 +543,27 @@ public abstract class StructureViewerBase extends GStructureViewer
         allChainsSelected = false;
       }
     });
-
     chainMenu.add(menuItem);
 
+    /*
+     * add a menu item for each structure and chain
+     */
+    Collections.sort(chainNames);
     for (String chain : chainNames)
     {
-      menuItem = new JCheckBoxMenuItem(chain, true);
+      String seqName = getSequenceNameForChain(chain);
+      if (seqName == null)
+      {
+        seqName = UNMAPPED;
+      }
+      int nameLength = seqName.length();
+      if (nameLength > 16)
+      {
+        seqName = seqName.substring(0, 8) + "..."
+                + seqName.substring(nameLength - 8, nameLength);
+      }
+      String text = chain + " " + seqName;
+      menuItem = new JCheckBoxMenuItem(text, true);
       menuItem.addItemListener(new ItemListener()
       {
         @Override
@@ -559,6 +581,26 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   /**
+   * Answers the name of the sequence mapped to the given chain (formatted as
+   * pdbId:chainId, e.g. 1A70:A). Answers null if no mapped sequence is found. If
+   * more than one sequence is matched, just answers the name of the first one
+   * found.
+   * 
+   * @param chain
+   * @return
+   */
+  private String getSequenceNameForChain(String chain)
+  {
+    String[] tokens = chain.split(":");
+    String pdbId = tokens[0];
+    String chainId = tokens[1];
+    List<StructureMapping> mappings = getBinding().getSsm()
+            .getMappingForChain(pdbId, chainId);
+    return mappings.isEmpty() ? null
+            : mappings.get(0).getSequence().getName();
+  }
+
+  /**
    * Action on selecting one of Jalview's registered colour schemes
    */
   @Override
@@ -714,6 +756,37 @@ public abstract class StructureViewerBase extends GStructureViewer
             });
     viewMenu.add(seqColourBy);
 
+    showAlignmentOnly = new JCheckBoxMenuItem(
+            MessageManager.getString("label.show_alignment_only"));
+    showAlignmentOnly.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        hideHiddenRegions.setEnabled(showAlignmentOnly.isSelected());
+        getBinding().setShowAlignmentOnly(showAlignmentOnly.isSelected());
+        getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
+                true);
+      }
+    });
+    viewMenu.add(showAlignmentOnly);
+
+    hideHiddenRegions = new JCheckBoxMenuItem(
+            MessageManager.getString("label.hide_hidden_regions"));
+    hideHiddenRegions.setEnabled(false);
+    hideHiddenRegions.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        getBinding().setHideHiddenRegions(hideHiddenRegions.isSelected());
+        getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
+                false);
+      }
+    });
+    viewMenu.add(hideHiddenRegions);
+    viewMenu.add(fitToWindow);
+
     final ItemListener handler = new ItemListener()
     {
       @Override
@@ -794,12 +867,12 @@ public abstract class StructureViewerBase extends GStructureViewer
       }
     } catch (Exception e)
     {
-      StringBuffer sp = new StringBuffer();
+      StringBuilder sb = new StringBuilder();
       for (AlignmentViewPanel alignPanel : _alignwith)
       {
-        sp.append("'" + alignPanel.getViewName() + "' ");
+        sb.append("'").append(alignPanel.getViewName()).append("' ");
       }
-      Cache.log.info("Couldn't align structures with the " + sp.toString()
+      Cache.log.info("Couldn't align structures with the " + sb.toString()
               + "associated alignment panels.", e);
     }
     return reply;
@@ -868,9 +941,9 @@ public abstract class StructureViewerBase extends GStructureViewer
         }
       }
       // Set the colour using the current view for the associated alignframe
-      for (AlignmentViewPanel alignPanel : _colourwith)
+      for (AlignmentViewPanel avp : _colourwith)
       {
-        binding.colourBySequence(alignPanel);
+        binding.updateStructureColours(avp);
       }
       seqColoursApplied = true;
     }
@@ -1045,6 +1118,23 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   @Override
+  public abstract AAStructureBindingModel getBinding();
+
+  /**
+   * Show only the selected chain(s) in the viewer
+   */
+  protected void showSelectedChains()
+  {
+    setChainsToHide();
+  
+    /*
+     * refresh display without resizing - easier to see what changed
+     */
+    getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
+            false);
+  }
+
+  @Override
   public long startProgressBar(String msg)
   {
     // TODO would rather have startProgress/stopProgress as the
@@ -1091,26 +1181,6 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   /**
-   * Show only the selected chain(s) in the viewer
-   */
-  protected void showSelectedChains()
-  {
-    List<String> toshow = new ArrayList<>();
-    for (int i = 0; i < chainMenu.getItemCount(); i++)
-    {
-      if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
-      {
-        JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
-        if (item.isSelected())
-        {
-          toshow.add(item.getText());
-        }
-      }
-    }
-    getBinding().showChains(toshow);
-  }
-
-  /**
    * Tries to fetch a PDB file and save to a temporary local file. Returns the
    * saved file path if successful, or null if not.
    * 
index a1529fc..ed9f013 100644 (file)
@@ -52,7 +52,7 @@ public class ViewSelectionMenu extends JMenu
 {
   public interface ViewSetProvider
   {
-    public AlignmentPanel[] getAllAlignmentPanels();
+    public AlignmentViewPanel[] getAllAlignmentPanels();
   }
 
   private ViewSetProvider _allviews;
@@ -132,7 +132,7 @@ public class ViewSelectionMenu extends JMenu
   private void rebuild()
   {
     removeAll();
-    AlignmentPanel[] allviews = _allviews.getAllAlignmentPanels();
+    AlignmentViewPanel[] allviews = _allviews.getAllAlignmentPanels();
     if (allviews == null)
     {
       setVisible(false);
@@ -209,12 +209,12 @@ public class ViewSelectionMenu extends JMenu
       invertSel.setEnabled(append);
       selectAll.setEnabled(append);
     }
-    for (final AlignmentPanel ap : allviews)
+    for (final AlignmentViewPanel ap : allviews)
     {
       String nm = ((ap.getViewName() == null
               || ap.getViewName().length() == 0) ? ""
                       : ap.getViewName() + " for ")
-              + ap.alignFrame.getTitle();
+              + ap.getTitle();
       final JCheckBoxMenuItem checkBox = new JCheckBoxMenuItem(nm,
               _selectedviews.contains(ap));
       checkBox.addItemListener(new ItemListener()
index 73180ee..6e88d27 100644 (file)
@@ -24,7 +24,10 @@ import java.awt.BorderLayout;
 import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.List;
 
+import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JMenu;
@@ -56,6 +59,10 @@ public abstract class GStructureViewer extends JInternalFrame
 
   protected JMenuItem alignStructs;
 
+  protected JCheckBoxMenuItem showAlignmentOnly;
+
+  protected JCheckBoxMenuItem hideHiddenRegions;
+
   protected JMenuItem fitToWindow;
 
   protected JRadioButtonMenuItem seqColour;
@@ -159,7 +166,7 @@ public abstract class GStructureViewer extends JInternalFrame
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        fitToWindow_actionPerformed();
+        getBinding().focusView();
       }
     });
 
@@ -212,10 +219,6 @@ public abstract class GStructureViewer extends JInternalFrame
     statusPanel.add(statusBar, null);
   }
 
-  protected void fitToWindow_actionPerformed()
-  {
-  }
-
   protected void highlightSelection_actionPerformed()
   {
   }
@@ -265,4 +268,27 @@ public abstract class GStructureViewer extends JInternalFrame
   {
 
   }
+
+  /**
+   * Saves the unselected entries in the 'View Chain' menu into a list. Entries
+   * are formatted as "pdbid:chainid". Unselected chains should be hidden in the
+   * structure display.
+   */
+  protected void setChainsToHide()
+  {
+    List<String> chains = new ArrayList<>();
+    for (int i = 0; i < chainMenu.getItemCount(); i++)
+    {
+      JMenuItem menuItem = chainMenu.getItem(i);
+      if (menuItem instanceof JCheckBoxMenuItem)
+      {
+        JCheckBoxMenuItem item = (JCheckBoxMenuItem) menuItem;
+        if (!item.isSelected())
+        {
+          chains.add(item.getText().split(" ")[0]);
+        }
+      }
+    }
+    getBinding().setChainsToHide(chains);
+  }
 }
index 3c29fd4..e4f7ac1 100644 (file)
@@ -16,6 +16,7 @@ import java.util.Map.Entry;
 public abstract class StructureCommandsBase implements StructureCommandsI
 {
   private static final String CMD_SEPARATOR = ";";
+
   public static final String NAMESPACE_PREFIX = "jv_";
 
   /**
index 53644e9..760cab0 100644 (file)
@@ -1161,6 +1161,28 @@ public class StructureSelectionManager
   }
 
   /**
+   * Answers a (possibly empty) list of structure to sequence mappings matching
+   * the given pdb and chain ids
+   * 
+   * @param pdbId
+   * @param chain
+   * @return
+   */
+  public List<StructureMapping> getMappingForChain(String pdbId,
+          String chain)
+  {
+    List<StructureMapping> result = new ArrayList<>();
+    for (StructureMapping sm : mappings)
+    {
+      if (sm.pdbid.equals(pdbId) && sm.pdbchain.equals(chain))
+      {
+        result.add(sm);
+      }
+    }
+    return result;
+  }
+
+  /**
    * Returns a readable description of all mappings for the given pdbfile to any
    * of the given sequences
    * 
index 5949847..7b1485c 100644 (file)
@@ -26,7 +26,9 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -168,13 +170,24 @@ public abstract class AAStructureBindingModel
 
   private boolean finishedInit = false;
 
-  /**
-   * current set of model filenames loaded in the viewer
+  /*
+   * current set of model filenames loaded in the Jmol instance 
+   * array index 0, 1, 2... corresponds to Jmol model numbers 1, 2, 3...
    */
   protected String[] modelFileNames = null;
 
   public String fileLoadingError;
 
+  private boolean showAlignmentOnly;
+
+  /*
+   * a list of chains "pdbid:chainid" to hide in the viewer
+   */
+  // TODO make private once deprecated JalviewJmolBinding.centerViewer removed
+  protected List<String> chainsToHide;
+
+  private boolean hideHiddenRegions;
+
   /**
    * Constructor
    * 
@@ -186,6 +199,7 @@ public abstract class AAStructureBindingModel
   {
     this.ssm = ssm;
     this.sequence = seqs;
+    chainsToHide = new ArrayList<>();
     chainNames = new ArrayList<>();
     chainFile = new HashMap<>();
   }
@@ -206,6 +220,8 @@ public abstract class AAStructureBindingModel
     this.nucleotide = Comparison.isNucleotide(sequenceIs);
     this.pdbEntry = pdbentry;
     this.protocol = protocol;
+    chainsToHide = new ArrayList<>();
+
     resolveChains();
   }
 
@@ -408,7 +424,7 @@ public abstract class AAStructureBindingModel
 
   /**
    * Instruct the Jalview binding to update the pdbentries vector if necessary
-   * prior to matching the jmol view's contents to the list of structure files
+   * prior to matching the viewer's contents to the list of structure files
    * Jalview knows about. By default does nothing, override as required.
    */
   public void refreshPdbEntries()
@@ -965,15 +981,37 @@ public abstract class AAStructureBindingModel
           AlignmentViewPanel alignment);
 
   /**
+   * Recolours mapped residues in the structure viewer to match colours in the
+   * given alignment panel, provided colourBySequence is selected. Colours
+   * should also be applied to any hidden mapped residues (so that they are
+   * shown correctly if these get unhidden).
+   * 
+   * @param viewPanel
+   */
+  protected void colourBySequence(AlignmentViewPanel viewPanel)
+  {
+
+    if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
+    {
+      return;
+    }
+    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
+            viewPanel);
+
+    List<StructureCommandI> colourBySequenceCommands = commandGenerator
+            .colourBySequence(colourMap);
+    executeCommands(colourBySequenceCommands, false, null);
+
+  }
+
+  /**
    * Sends a command to the structure viewer to colour each chain with a
    * distinct colour (to the extent supported by the viewer)
    */
   public void colourByChain()
   {
     colourBySequence = false;
-
     // TODO: JAL-628 colour chains distinctly across all visible models
-
     executeCommand(commandGenerator.colourByChain(), false,
             COLOURING_STRUCTURES);
   }
@@ -1165,21 +1203,36 @@ public abstract class AAStructureBindingModel
   }
 
   /**
-   * Colours any structures associated with sequences in the given alignment as
-   * coloured in the alignment view, provided colourBySequence is enabled
+   * Recolours the displayed structures, if they are coloured by
+   * sequence, or 'show only visible alignment' is selected. This supports
+   * updating structure colours on either change of alignment colours, or change
+   * to the visible region of the alignment.
    */
-  public void colourBySequence(AlignmentViewPanel alignmentv)
+  public void updateStructureColours(AlignmentViewPanel alignmentv)
   {
-    if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
+    if (!isLoadingFinished())
     {
       return;
     }
-    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
-            alignmentv);
 
-    List<StructureCommandI> colourBySequenceCommands = commandGenerator
-            .colourBySequence(colourMap);
-    executeCommands(colourBySequenceCommands, false, null);
+    /*
+     * if structure is not coloured by sequence, but restricted to the alignment,
+     * then redraw it (but don't recolour it) in case hidden regions have changed
+     * (todo: specific messaging for change of hidden region only)
+     */
+    if (!colourBySequence)
+    {
+      if (isShowAlignmentOnly())
+      {
+        showStructures(alignmentv.getAlignViewport(), false);
+      }
+      return;
+    }
+    if (getSsm() == null)
+    {
+      return;
+    }
+    colourBySequence(alignmentv);
   }
 
   /**
@@ -1235,6 +1288,202 @@ public abstract class AAStructureBindingModel
   }
 
   /**
+   * Sets the flag for whether only mapped visible residues in the alignment
+   * should be visible in the structure viewer
+   * 
+   * @param b
+   */
+  public void setShowAlignmentOnly(boolean b)
+  {
+    showAlignmentOnly = b;
+  }
+
+  /**
+   * Answers true if only residues mapped to the alignment should be shown in the
+   * structure viewer, else false
+   * 
+   * @return
+   */
+  public boolean isShowAlignmentOnly()
+  {
+    return showAlignmentOnly;
+  }
+
+  /**
+   * Sets the flag for hiding regions of structure which are hidden in the
+   * alignment (only applies when the structure viewer is restricted to the
+   * alignment only)
+   * 
+   * @param b
+   */
+  public void setHideHiddenRegions(boolean b)
+  {
+    hideHiddenRegions = b;
+  }
+
+  /**
+   * Answers true if regions hidden in the alignment should also be hidden in the
+   * structure viewer, else false (only applies when the structure viewer is
+   * restricted to the alignment only)
+   * 
+   * @return
+   */
+  public boolean isHideHiddenRegions()
+  {
+    return hideHiddenRegions;
+  }
+
+  /**
+   * Shows the structures in the viewer, without changing their colouring. This is
+   * to support toggling of whether the whole structure is shown, or only residues
+   * mapped to visible regions of the alignment.
+   * 
+   * @param alignViewportI
+   * @param refocus
+   *                         if true, refit the display to the viewer
+   */
+  public void showStructures(AlignViewportI alignViewportI, boolean refocus)
+  {
+    // override with implementation
+  }
+
+  /**
+   * Sets the list of chains to hide (as "pdbid:chain")
+   * 
+   * @param chains
+   */
+  public void setChainsToHide(List<String> chains)
+  {
+    chainsToHide = chains;
+  }
+
+  /**
+   * Answers true if the specified structure and chain are selected to be shown in
+   * the viewer, else false
+   * 
+   * @param pdbId
+   * @param chainId
+   * @return
+   */
+  protected boolean isShowChain(String pdbId, String chainId)
+  {
+    if (chainsToHide.isEmpty())
+    {
+      return true;
+    }
+    return !chainsToHide.contains(pdbId + ":" + chainId);
+  }
+
+  @Override
+  public abstract String[] getStructureFiles();
+
+  /**
+   * Builds a model of residues mapped from sequences to show on structure, taking
+   * into account user choices of
+   * <ul>
+   * <li>which chains are shown</li>
+   * <li>whether all structure is shown, or only that mapped to the alignment</li>
+   * <li>whether hidden regions of the alignment are hidden (excluded) or grayed
+   * out (included)</li>
+   * </ul>
+   * 
+   * @param av
+   * @return
+   */
+  protected AtomSpecModel getShownResidues(AlignViewportI av)
+  {
+    AlignmentI alignment = av.getAlignment();
+    final int width = alignment.getWidth();
+  
+    String[] files = getStructureFiles();
+  
+    AtomSpecModel model = new AtomSpecModel();
+  
+    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
+    {
+      String fileName = files[pdbfnum];
+      final String modelId = getModelIdForFile(files[pdbfnum]);
+      StructureMapping[] mappings = getSsm().getMapping(fileName);
+  
+      /*
+       * Find the first mapped sequence (if any) for this PDB entry which is in
+       * the alignment
+       */
+      final int seqCountForPdbFile = getSequence()[pdbfnum].length;
+      for (int s = 0; s < seqCountForPdbFile; s++)
+      {
+        for (StructureMapping mapping : mappings)
+        {
+          final SequenceI theSequence = getSequence()[pdbfnum][s];
+          if (mapping.getSequence() == theSequence
+                  && alignment.findIndex(theSequence) > -1)
+          {
+            String chainCd = mapping.getChain();
+            if (!isShowChain(mapping.getPdbId(), chainCd))
+            {
+              // continue;
+            }
+            Iterator<int[]> visible;
+            if (isShowAlignmentOnly() && isHideHiddenRegions())
+            {
+              visible = alignment.getHiddenColumns()
+                    .getVisContigsIterator(0, width, true);
+            }
+            else
+            {
+              visible = Collections.singletonList(new int[] { 0, width })
+                      .iterator();
+            }
+            while (visible.hasNext())
+            {
+              int[] visibleRegion = visible.next();
+              int seqStartPos = theSequence.findPosition(visibleRegion[0]);
+              int seqEndPos = theSequence.findPosition(visibleRegion[1]);
+              List<int[]> residueRanges = mapping
+                      .getPDBResNumRanges(seqStartPos, seqEndPos);
+              if (!residueRanges.isEmpty())
+              {
+                for (int[] range : residueRanges)
+                {
+                  model.addRange(modelId, range[0], range[1], chainCd);
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  
+    return model;
+  }
+
+  /**
+   * Answers the structure viewer's model number for the given PDB file, or -1 if
+   * not found
+   * 
+   * @param fileName
+   * @param fileIndex
+   *                    index of the file in the stored array of file names
+   * @return
+   */
+  public int getModelForPdbFile(String fileName, int fileIndex)
+  {
+    return fileIndex;
+  }
+
+  /**
+   * Answers a default structure model specification which is simply the string
+   * form of the model number. Override if needed to specify submodels.
+   * 
+   * @param model
+   * @return
+   */
+  public String getModelSpec(int model)
+  {
+    return String.valueOf(model);
+  }
+
+  /**
    * Returns the FeatureRenderer for the given alignment view, or null if
    * feature display is turned off in the view.
    * 
@@ -1294,7 +1543,7 @@ public abstract class AAStructureBindingModel
     }
     if (!isLoadingFromArchive())
     {
-      colourBySequence(ap);
+      updateStructureColours(ap);
     }
   }
 
diff --git a/src/jalview/util/StructureCommands.java b/src/jalview/util/StructureCommands.java
new file mode 100644 (file)
index 0000000..b45ca5d
--- /dev/null
@@ -0,0 +1,273 @@
+package jalview.util;
+
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.api.SequenceRenderer;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.SequenceI;
+import jalview.ext.rbvi.chimera.AtomSpecModel;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
+import jalview.structure.StructureMapping;
+import jalview.structures.models.AAStructureBindingModel;
+
+import java.awt.Color;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class with common methods for building commands for Jmol, Chimera, or other
+ * 
+ * @author gmcarstairs
+ */
+public abstract class StructureCommands
+{
+
+  /**
+   * 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 value
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public 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(value);
+    if (atomSpec == null)
+    {
+      atomSpec = new AtomSpecModel();
+      map.put(value, atomSpec);
+    }
+  
+    atomSpec.addRange(model, startPos, endPos, chain);
+  }
+
+  /**
+   * Build a data structure which records contiguous subsequences by colour, model
+   * and chain. From this we can easily generate the Chimera or Jmol specific
+   * selection expression.
+   * 
+   * <pre>
+   * Color
+   *     Model number
+   *         Chain
+   *             list of start/end ranges
+   * </pre>
+   * 
+   * Ordering is by order of addition (for colours and positions), natural
+   * ordering (for models and chains)
+   * 
+   * @param viewPanel
+   * @return
+   */
+  public static Map<Object, AtomSpecModel> buildColoursMap(
+          AAStructureBindingModel binding, AlignmentViewPanel viewPanel)
+  {
+    FeatureRenderer fr = viewPanel.getFeatureRenderer();
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
+    AlignViewportI viewport = viewPanel.getAlignViewport();
+    HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
+    AlignmentI al = viewport.getAlignment();
+    SequenceRenderer sr = binding.getSequenceRenderer(viewPanel);
+    String[] files = binding.getStructureFiles();
+    SequenceI[][] sequence = binding.getSequence();
+    
+    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
+    Color lastColour = null;
+  
+    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
+    {
+      String fileName = files[pdbfnum];
+      final int modelNumber = binding.getModelForPdbFile(fileName, pdbfnum);
+      StructureMapping[] mapping = binding.getSsm()
+              .getMapping(fileName);
+  
+      if (mapping == null || mapping.length < 1)
+      {
+        continue;
+      }
+  
+      int startPos = -1, lastPos = -1;
+      String lastChain = "";
+      for (int s = 0; s < sequence[pdbfnum].length; s++)
+      {
+        for (int sp, m = 0; m < mapping.length; m++)
+        {
+          final SequenceI seq = sequence[pdbfnum][s];
+          if (mapping[m].getSequence() == seq
+                  && (sp = al.findIndex(seq)) > -1)
+          {
+            SequenceI asp = al.getSequenceAt(sp);
+            for (int r = 0; r < asp.getLength(); r++)
+            {
+              // no mapping to gaps in sequence
+              if (Comparison.isGap(asp.getCharAt(r)))
+              {
+                continue;
+              }
+              int pos = mapping[m].getPDBResNum(asp.findPosition(r));
+  
+              if (pos < 1 || pos == lastPos)
+              {
+                continue;
+              }
+  
+              Color colour = sr.getResidueColour(seq, r, finder);
+  
+              /*
+               * hidden regions are shown gray or, optionally, ignored
+               */
+              if (!cs.isVisible(r))
+              {
+                if (binding.isHideHiddenRegions())
+                {
+                  continue;
+                }
+                else
+                {
+                  colour = Color.GRAY;
+                }
+              }
+  
+              final String chain = mapping[m].getChain();
+  
+              /*
+               * Just keep incrementing the end position for this colour range
+               * _unless_ colour, PDB model or chain has changed, or there is a
+               * gap in the mapped residue sequence
+               */
+              final boolean newColour = !colour.equals(lastColour);
+              final boolean nonContig = lastPos + 1 != pos;
+              final boolean newChain = !chain.equals(lastChain);
+              if (newColour || nonContig || newChain)
+              {
+                if (startPos != -1)
+                {
+                  StructureCommands.addAtomSpecRange(colourMap, lastColour,
+                          modelNumber, startPos, lastPos, lastChain);
+                }
+                startPos = pos;
+              }
+              lastColour = colour;
+              lastPos = pos;
+              lastChain = chain;
+            }
+            // final colour range
+            if (lastColour != null)
+            {
+              StructureCommands.addAtomSpecRange(colourMap, lastColour,
+                      modelNumber, startPos, lastPos, lastChain);
+            }
+          }
+        }
+      }
+    }
+    return colourMap;
+  }
+
+  /**
+   * A helper method that takes a list of [start-end] ranges, format them as
+   * s1-e1,s2-e2 etc and appends to the string buffer. Ranges are sorted, and
+   * coalesced if they overlap. The chain token, if not null, is appended to each
+   * resulting subrange.
+   * 
+   * @param sb
+   * @param rangeList
+   * @param chainToken
+   * @param firstPositionForModel
+   */
+  protected static void appendResidueRange(StringBuilder sb, List<int[]> rangeList,
+          String chainToken, boolean firstPositionForModel)
+  {
+    /*
+     * sort ranges into ascending start position order
+     */
+    Collections.sort(rangeList, IntRangeComparator.ASCENDING);
+  
+    int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
+    int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
+  
+    Iterator<int[]> iterator = rangeList.iterator();
+    while (iterator.hasNext())
+    {
+      int[] range = iterator.next();
+      if (range[0] <= end + 1)
+      {
+        /*
+         * range overlaps or is contiguous with the last one
+         * - so just extend the end position, and carry on
+         * (unless this is the last in the list)
+         */
+        end = Math.max(end, range[1]);
+      }
+      else
+      {
+        /*
+         * we have a break so append the last range
+         */
+        appendRange(sb, start, end, chainToken, firstPositionForModel);
+        firstPositionForModel = false;
+        start = range[0];
+        end = range[1];
+      }
+    }
+  
+    /*
+     * and append the last range
+     */
+    if (!rangeList.isEmpty())
+    {
+      appendRange(sb, start, end, chainToken, firstPositionForModel);
+    }
+  }
+
+  /**
+   * A helper method that appends one start-end range, and an (optional) chain
+   * token to an atomspec (a null token is not appended)
+   * 
+   * @param sb
+   * @param start
+   * @param end
+   * @param chainToken
+   * @param firstPositionForModel
+   */
+  protected static void appendRange(StringBuilder sb, int start, int end,
+          String chainToken, boolean firstPositionForModel)
+  {
+    if (!firstPositionForModel)
+    {
+      sb.append(",");
+    }
+    if (end == start)
+    {
+      sb.append(start);
+    }
+    else
+    {
+      sb.append(start).append("-").append(end);
+    }
+    if (chainToken != null)
+    {
+      sb.append(chainToken);
+    }
+  }
+
+}
diff --git a/test/jalview/ext/jmol/JalviewJmolBindingTest.java b/test/jalview/ext/jmol/JalviewJmolBindingTest.java
new file mode 100644 (file)
index 0000000..0a0b634
--- /dev/null
@@ -0,0 +1,160 @@
+package jalview.ext.jmol;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.gui.AppJmolBinding;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.structure.StructureMapping;
+import jalview.structure.StructureSelectionManager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class JalviewJmolBindingTest
+{
+  private AlignFrame af;
+
+  @BeforeTest(alwaysRun = true)
+  public void setup()
+  {
+    af = new FileLoader().LoadFileWaitTillLoaded("examples/uniref50.fa",
+            DataSourceType.FILE);
+  }
+
+  @Test(groups = "Functional")
+  public void testBuildShowStructuresCommand()
+  {
+    AlignViewport av = af.getViewport();
+    PDBEntry[] pdbs = new PDBEntry[] {};
+    SequenceI seq1 = av.getAlignment().findSequenceMatch("FER1_SPIOL")[0];
+    assertNotNull(seq1);
+    SequenceI seq2 = av.getAlignment().findSequenceMatch("FER2_ARATH")[0];
+    assertNotNull(seq2);
+    StructureSelectionManager ssm = new StructureSelectionManager();
+    SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
+    AppJmolBinding testee = new AppJmolBinding(null, ssm, pdbs, seqs,
+            null);
+
+    /*
+     * map FER1_SPIOL residues 51-100 to residues 1-50 (atoms 1-250) in 1A70
+     * and residues 110-147 to structure residues 60-97
+     * (in fact there is no gap, added here for test purposes)
+     */
+    HashMap<Integer, int[]> map = new HashMap<>();
+    for (int pos = 51; pos <= 100; pos++)
+    {
+      map.put(pos, new int[] { pos - 50, 5 * (pos - 50) });
+    }
+    for (int pos = 110; pos <= 147; pos++)
+    {
+      map.put(pos, new int[] { pos - 50, 5 * (pos - 50) });
+    }
+    StructureMapping sm1 = new StructureMapping(seq1, "1a70.pdb", "1A70",
+            "A", map, null);
+    ssm.addStructureMapping(sm1);
+
+    /*
+     * map FER2_ARATH residues 53-148 to residues 2-97 in 4ZHO
+     */
+    map = new HashMap<>();
+    for (int pos = 53; pos <= 148; pos++)
+    {
+      map.put(pos, new int[] { pos - 51, 5 * (pos - 51) });
+    }
+    StructureMapping sm2 = new StructureMapping(seq2, "4zho.pdb", "4ZHO",
+            "B", map, null);
+    ssm.addStructureMapping(sm2);
+
+    /*
+     * show everything
+     */
+    String cmd = testee.buildShowStructuresCommand(av, true);
+    assertEquals(cmd, "display *; cartoon only; zoom 0");
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd, "display *; cartoon only");
+
+    /*
+     * stub out modelFileNames - array index is Jmol
+     * model number - 1
+     */
+    PA.setValue(testee, "modelFileNames",
+            new String[]
+            { "1a70.pdb", "4zho.pdb" });
+
+    /*
+     * stub out lookup map from pdb:chain to filename
+     */
+    Map<String, String> chainFiles = new HashMap<>();
+    PA.setValue(testee, "chainFile", chainFiles);
+    chainFiles.put("1A70:A", "1a70.pdb");
+    chainFiles.put("1A70:B", "1a70.pdb");
+    chainFiles.put("4ZHO:B", "4zho.pdb");
+    chainFiles.put("4ZHO:C", "4zho.pdb");
+
+    /*
+     * show all except for selected chains to hide
+     */
+    List<String> chainsToHide = (List<String>) PA.getValue(testee,
+            "chainsToHide");
+    chainsToHide.add("1A70:B");
+    chainsToHide.add("4ZHO:C");
+    cmd = testee.buildShowStructuresCommand(av, true);
+    assertEquals(cmd,
+            "display *; hide add :B/1.1,:C/2.1; cartoon only; zoom 0");
+
+    /*
+     * show alignment only, no chains hidden
+     */
+    chainsToHide.clear();
+    testee.setShowAlignmentOnly(true);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd,
+            "hide *;display (1-50,60-97)&:A/1.1,2-97:B/2.1; select displayed; cartoon only");
+
+    /*
+     * now with a chain hidden
+     */
+    chainsToHide.add("4ZHO:C");
+    cmd = testee.buildShowStructuresCommand(av, false);
+    String expected = "hide *;display (1-50,60-97)&:A/1.1,2-97:B/2.1; select displayed; hide add :C/2.1; cartoon only";
+    assertEquals(cmd, expected);
+
+    /*
+     * hide columns in the mapped region - should not change the command (yet)
+     */
+    int fromCol = seq1.findIndex(60); // structure residue 10
+    int toCol = seq1.findIndex(70); // structure residue 20
+    av.hideColumns(fromCol - 1, toCol - 1);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd, expected);
+
+    /*
+     * select 'hide hidden columns'
+     * command should now exclude these in both mapped sequences
+     */
+    testee.setHideHiddenRegions(true);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    expected = "hide *;display (1-9,21-50,60-97)&:A/1.1,(2-10,22-97)&:B/2.1; select displayed; hide add :C/2.1; cartoon only";
+    assertEquals(cmd, expected);
+
+    /*
+     * deselect 'show alignment only'
+     * hide hidden columns is now ignored
+     */
+    testee.setShowAlignmentOnly(false);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd, "display *; hide add :C/2.1; cartoon only");
+  }
+}
index 21f7e19..c0bb629 100644 (file)
@@ -55,7 +55,10 @@ public class JmolCommandsTest
     testee = new JmolCommands();
   }
 
-  @Test(groups = { "Functional" })
+  /**
+   * Test for the now deprecated version of getColourBySequenceCommand
+   */
+  @Test(groups = { "Functional" }, enabled = false)
   public void testGetColourBySequenceCommands_hiddenColumns()
   {
     /*
@@ -93,16 +96,15 @@ public class JmolCommandsTest
             "B", map, null);
     ssm.addStructureMapping(sm2);
 
-    String[] commands = testee.colourBySequence(ssm,
-            files,
-            seqs, sr, af.alignPanel);
+    String[] commands = testee.colourBySequence(ssm, files, seqs,
+            sr,
+            af.alignPanel);
     assertEquals(commands.length, 2);
 
     String chainACommand = commands[0];
     // M colour is #82827d == (130, 130, 125) (see strand.html help page)
     assertTrue(
-            chainACommand.contains("select 21:A/1.1;color[130,130,125]")); // first
-                                                                           // one
+            chainACommand.contains("select 21:A/1.1;color[130,130,125]")); // first one
     // H colour is #60609f == (96, 96, 159)
     assertTrue(chainACommand.contains(";select 22:A/1.1;color[96,96,159]"));
     // hidden columns are Gray (128, 128, 128)
@@ -126,40 +128,65 @@ public class JmolCommandsTest
             chainBCommand.contains(";select 26-30:B/2.1;color[73,73,182]"));
   }
 
-  @Test(groups = "Functional")
+  @Test(groups = { "Functional" })
   public void testGetAtomSpec()
   {
     AtomSpecModel model = new AtomSpecModel();
     assertEquals(testee.getAtomSpec(model, false), "");
-    model.addRange("1", 2, 4, "A");
-    assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1");
-    model.addRange("1", 8, 8, "A");
-    assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1|8:A/1.1");
-    model.addRange("1", 5, 7, "B");
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-4:A/1.1|8:A/1.1|5-7:B/1.1");
-    model.addRange("1", 3, 5, "A");
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-5:A/1.1|8:A/1.1|5-7:B/1.1");
-    model.addRange("2", 1, 4, "B");
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1");
-    model.addRange("2", 5, 9, "C");
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1|5-9:C/2.1");
-    model.addRange("1", 8, 10, "B");
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1");
-    model.addRange("1", 8, 9, "B");
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1");
-    model.addRange("2", 3, 10, "C"); // subsumes 5-9
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1");
-    model.addRange("5", 25, 35, " ");
-    assertEquals(testee.getAtomSpec(model, false),
-            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1|25-35:/5.1");
 
+    /*
+     * Jalview numbers models from 0, Jmol from 1
+     */
+    model.addRange("2", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "2-4:A/2.1");
+
+    model.addRange("2", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "(2-4,8)&:A/2.1");
+
+    model.addRange("2", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "(2-4,8)&:A/2.1,5-7:B/2.1");
+
+    model.addRange("2", 3, 5, "A");
+    // 3-5 merges with 2-4 to make 2-5:
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "(2-5,8)&:A/2.1,5-7:B/2.1");
+
+    model.addRange("1", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "1-4:B/1.1,(2-5,8)&:A/2.1,5-7:B/2.1");
+
+    model.addRange("1", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "1-4:B/1.1,5-9:C/1.1,(2-5,8)&:A/2.1,5-7:B/2.1");
+
+    model.addRange("2", 8, 10, "B");
+    // 8-10 extends 5-7 to make 5-10
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "1-4:B/1.1,5-9:C/1.1,(2-5,8)&:A/2.1,5-10:B/2.1");
+
+    model.addRange("2", 8, 9, "B");
+    // subsumed by 5-10 so makes no difference
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "1-4:B/1.1,5-9:C/1.1,(2-5,8)&:A/2.1,5-10:B/2.1");
+
+    model.addRange("1", 3, 10, "C");
+    // subsumes 5-9 so replaces it
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "1-4:B/1.1,3-10:C/1.1,(2-5,8)&:A/2.1,5-10:B/2.1");
+
+    // empty chain code - e.g. from homology modelling
+    model.addRange("6", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model,
+            false),
+            "1-4:B/1.1,3-10:C/1.1,(2-5,8)&:A/2.1,5-10:B/2.1,25-35:/6.1");
   }
 
   @Test(groups = { "Functional" })
@@ -180,8 +207,8 @@ public class JmolCommandsTest
     // they were added; within colour, by model, by chain, ranges in start order
     List<StructureCommandI> commands = testee.colourBySequence(map);
     assertEquals(commands.size(), 1);
-    String expected1 = "select 2-5:A/1.1|9-23:A/1.1|7:B/1.1|1:A/2.1|4-7:B/2.1;color[0,0,255]";
-    String expected2 = "select 3-5:A/2.1|8:A/2.1;color[255,255,0]";
+    String expected1 = "select (2-5,9-23)&:A/1.1,7:B/1.1,1:A/2.1,4-7:B/2.1;color[0,0,255]";
+    String expected2 = "select (3-5,8)&:A/2.1;color[255,255,0]";
     String expected3 = "select 3-9:A/1.1;color[255,0,0]";
     assertEquals(commands.get(0).getCommand(),
             expected1 + ";" + expected2 + ";" + expected3);
@@ -201,8 +228,8 @@ public class JmolCommandsTest
     List<StructureCommandI> command = testee.superposeStructures(ref,
             toAlign);
     assertEquals(command.size(), 1);
-    String refSpec = "12-14:A/1.1|18:B/1.1|22-23:B/1.1";
-    String toAlignSpec = "15-17:B/2.1|20-21:B/2.1|22:C/2.1";
+    String refSpec = "12-14:A/1.1,(18,22-23)&:B/1.1";
+    String toAlignSpec = "(15-17,20-21)&:B/2.1,22:C/2.1";
     String expected = String.format(
             "compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS {%s}{%s} ROTATE TRANSLATE ;select %s|%s;cartoons",
             toAlignSpec, refSpec, toAlignSpec, refSpec);
index b040687..d8eb7bc 100644 (file)
@@ -48,7 +48,6 @@ public class ChimeraCommandsTest
   @Test(groups = { "Functional" })
   public void testColourBySequence()
   {
-
     Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
     ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 2, 5, "A");
     ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 7, 7, "B");
@@ -62,10 +61,11 @@ public class ChimeraCommandsTest
 
     // 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
+    // all prefixed with #808080 to colour hidden regions (if shown) gray
     List<StructureCommandI> commands = testee.colourBySequence(map);
     assertEquals(commands.size(), 1);
     assertEquals(commands.get(0).getCommand(),
-            "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B;color #ffff00 #1:3-5.A,8.A;color #ff0000 #0:3-9.A");
+            "color #808080; color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B;color #ffff00 #1:3-5.A,8.A;color #ff0000 #0:3-9.A");
   }
 
   @Test(groups = { "Functional" })
@@ -248,7 +248,6 @@ public class ChimeraCommandsTest
     model.addRange("5", 25, 35, " "); // empty chain code
     assertEquals(testee.getAtomSpec(model, true),
             "#0:1-4.B,3-10.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9|#5:25-35.@CA&~@.B-Z&~@.2-9");
-
   }
 
   @Test(groups = "Functional")
diff --git a/test/jalview/ext/rbvi/chimera/JalviewChimeraBindingTest.java b/test/jalview/ext/rbvi/chimera/JalviewChimeraBindingTest.java
new file mode 100644 (file)
index 0000000..fde4bb5
--- /dev/null
@@ -0,0 +1,166 @@
+package jalview.ext.rbvi.chimera;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.gui.ChimeraViewFrame;
+import jalview.gui.JalviewChimeraBindingModel;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureMapping;
+import jalview.structure.StructureSelectionManager;
+import junit.extensions.PA;
+
+public class JalviewChimeraBindingTest
+{
+  private AlignFrame af;
+
+  @BeforeTest(alwaysRun = true)
+  public void setup()
+  {
+    af = new FileLoader().LoadFileWaitTillLoaded("examples/uniref50.fa",
+            DataSourceType.FILE);
+  }
+
+  @Test(groups = "Functional")
+  public void testBuildShowStructuresCommand()
+  {
+    AlignViewport av = af.getViewport();
+    PDBEntry[] pdbs = new PDBEntry[] {};
+    StructureSelectionManager ssm = new StructureSelectionManager();
+    SequenceI seq1 = av.getAlignment().findSequenceMatch("FER1_SPIOL")[0];
+    assertNotNull(seq1);
+    SequenceI seq2 = av.getAlignment().findSequenceMatch("FER2_ARATH")[0];
+    assertNotNull(seq2);
+    SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
+    JalviewChimeraBindingModel testee = new JalviewChimeraBindingModel(
+            new ChimeraViewFrame(),
+            ssm, pdbs, seqs, null);
+
+    /*
+     * with no structures mapped
+     */
+    StructureCommandI cmd = testee.buildShowStructuresCommand(av, true);
+    assertEquals(cmd.getCommand(), "~display; ribbon; focus");
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd.getCommand(), "~display; ribbon");
+
+    /*
+     * stub out a structure with chains A and B
+     */
+    Map<String, List<ChimeraModel>> chimeraMaps = (Map<String, List<ChimeraModel>>) PA
+            .getValue(testee, "chimeraMaps");
+    ChimeraModel model0 = new ChimeraModel("1A70", ModelType.PDB_MODEL, 0,
+            0);
+    chimeraMaps.put("1a70.pdb", Arrays.asList(model0));
+    ChimeraModel model1 = new ChimeraModel("4ZHO", ModelType.PDB_MODEL, 1,
+            0);
+    chimeraMaps.put("4zho.pdb", Arrays.asList(model1));
+
+    Map<String, String> chainFiles = (Map<String, String>) PA
+            .getValue(testee, "chainFile");
+    chainFiles.put("1A70:A", "1a70.pdb");
+    chainFiles.put("1A70:B", "1a70.pdb");
+    chainFiles.put("4ZHO:B", "4zho.pdb");
+    chainFiles.put("4ZHO:C", "4zho.pdb");
+
+    /*
+     * map FER1_SPIOL residues 51-100 to residues 1-50 (atoms 1-250) in 1A70
+     * and residues 110-147 to structure residues 60-97
+     * (in fact there is no gap, added here for test purposes)
+     */
+    HashMap<Integer, int[]> map = new HashMap<>();
+    for (int pos = 51; pos <= 100; pos++)
+    {
+      map.put(pos, new int[] { pos - 50, 5 * (pos - 50) });
+    }
+    for (int pos = 110; pos <= 147; pos++)
+    {
+      map.put(pos, new int[] { pos - 50, 5 * (pos - 50) });
+    }
+    StructureMapping sm1 = new StructureMapping(seq1, "1a70.pdb", "1A70",
+            "A", map, null);
+    ssm.addStructureMapping(sm1);
+
+    /*
+     * map FER2_ARATH residues 53-148 to residues 2-97 in 4ZHO
+     */
+    map = new HashMap<>();
+    for (int pos = 53; pos <= 148; pos++)
+    {
+      map.put(pos, new int[] { pos - 51, 5 * (pos - 51) });
+    }
+    StructureMapping sm2 = new StructureMapping(seq2, "4zho.pdb", "4ZHO",
+            "B", map, null);
+    ssm.addStructureMapping(sm2);
+
+    /*
+     * select chain A only (hide chain B)
+     */
+    List<String> chainsToHide = (List<String>) PA.getValue(testee, "chainsToHide");
+    chainsToHide.add("1A70:B");
+    chainsToHide.add("4ZHO:C");
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd.getCommand(),
+            "~display; ribbon; ~ribbon #0:.B; ~ribbon #1:.C");
+
+    /*
+     * show alignment only, no chains hidden
+     */
+    chainsToHide.clear();
+    testee.setShowAlignmentOnly(true);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd
+            .getCommand(),
+            "~display; ~ribbon; ribbon #0:1-50.A,60-97.A|#1:2-97.B");
+
+    /*
+     * now with a chain hidden
+     */
+    chainsToHide.add("4ZHO:C");
+    cmd = testee.buildShowStructuresCommand(av, false);
+    String expected = "~display; ~ribbon; ribbon #0:1-50.A,60-97.A|#1:2-97.B; ~ribbon #1:.C";
+    assertEquals(cmd.getCommand(), expected);
+
+    /*
+     * hide columns in the mapped region - should not change the command (yet)
+     */
+    int fromCol = seq1.findIndex(60); // structure residue 10
+    int toCol = seq1.findIndex(70); // structure residue 20
+    av.hideColumns(fromCol - 1, toCol - 1);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd.getCommand(), expected);
+
+    /*
+     * select 'hide hidden columns'
+     * command should now exclude these in both mapped sequences
+     */
+    testee.setHideHiddenRegions(true);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    expected = "~display; ~ribbon; ribbon #0:1-9.A,21-50.A,60-97.A|#1:2-10.B,22-97.B; ~ribbon #1:.C";
+    assertEquals(cmd.getCommand(), expected);
+
+    /*
+     * deselect 'show alignment only'
+     * hide hidden columns is now ignored
+     */
+    testee.setShowAlignmentOnly(false);
+    cmd = testee.buildShowStructuresCommand(av, false);
+    assertEquals(cmd.getCommand(), "~display; ribbon; ~ribbon #1:.C");
+  }
+}
index c1ad03a..aea12b4 100644 (file)
@@ -23,7 +23,7 @@ package jalview.structures.models;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertTrue;
 
 import java.awt.Color;
 import java.io.IOException;
@@ -135,61 +135,13 @@ public class AAStructureBindingModelTest
     // ideally, we would match on the actual data for the 'File' handle for
     // pasted files,
     // see JAL-623 - pasting is still not correctly handled...
-    PDBEntry importedPDB = new PDBEntry("3A6S", "", Type.PDB,
-            "Paste");
-    AAStructureBindingModel binder = new AAStructureBindingModel(
-            new StructureSelectionManager(), new PDBEntry[]
+    PDBEntry importedPDB = new PDBEntry("3A6S", "", Type.PDB, "Paste");
+    AAStructureBindingModel binder = newBindingModel(new PDBEntry[]
             { importedPDB },
             new SequenceI[][]
-            { importedAl.getSequencesArray() }, null)
-    {
-      
-      @Override
-      public void updateColours(Object source)
-      {
-      }
-      
-      @Override
-      public void releaseReferences(Object svl)
-      {
-      }
-      
-      @Override
-      public String[] getStructureFiles()
-      {
-        return null;
-      }
-      
-      @Override
-      public void highlightAtoms(List<AtomSpec> atoms)
-      {
-      }
-      
-      @Override
-      public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
-      {
-        return null;
-      }
+            { importedAl.getSequencesArray() },
+            new StructureSelectionManager(), null);
 
-      @Override
-      protected List<String> executeCommand(StructureCommandI command,
-              boolean getReply)
-      {
-        return null;
-      }
-
-      @Override
-      protected String getModelIdForFile(String chainId)
-      {
-        return "";
-      }
-
-      @Override
-      protected ViewerType getViewerType()
-      {
-        return null;
-      }
-    };
     String[][] chains = binder.getChains();
     assertFalse(chains == null || chains[0] == null,
             "No chains discovered by binding");
@@ -279,6 +231,11 @@ public class AAStructureBindingModelTest
       }
 
       @Override
+      public void setBackgroundColour(Color col)
+      {
+      }
+
+      @Override
       public SequenceRenderer getSequenceRenderer(
               AlignmentViewPanel avp)
       {
index 7a80303..5e4c948 100644 (file)
Binary files a/utils/testnglibs/testng-sources.jar and b/utils/testnglibs/testng-sources.jar differ