Merge branch 'develop' into features/JAL-2295setChimeraAttributes
[jalview.git] / src / jalview / ext / rbvi / chimera / JalviewChimeraBinding.java
index 415fc1b..cc1de6a 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.9)
- * Copyright (C) 2015 The Jalview Authors
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
@@ -27,6 +27,9 @@ import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SearchResultMatchI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.httpserver.AbstractRequestHandler;
 import jalview.schemes.ColourSchemeI;
@@ -38,8 +41,14 @@ import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.MessageManager;
 
 import java.awt.Color;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
 import java.net.BindException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Hashtable;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -51,6 +60,8 @@ import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
 
 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
 {
+  public static final String CHIMERA_FEATURE_GROUP = "Chimera";
+
   // Chimera clause to exclude alternate locations in atom selection
   private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
 
@@ -63,6 +74,10 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
 
   private static final String ALPHACARBON = "CA";
 
+  private List<String> chainNames = new ArrayList<String>();
+
+  private Hashtable<String, String> chainFile = new Hashtable<String, String>();
+  
   /*
    * Object through which we talk to Chimera
    */
@@ -93,24 +108,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    */
   private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<String, List<ChimeraModel>>();
 
-  /*
-   * the default or current model displayed if the model cannot be identified
-   * from the selection message
-   */
-  private int frameNo = 0;
-
-  private String lastCommand;
-
-  private boolean loadedInline;
-
-  /**
-   * current set of model filenames loaded
-   */
-  String[] modelFileNames = null;
-
-  String lastMousedOverAtomSpec;
-
-  private List<String> lastReply;
+  String lastHighlightCommand;
 
   /*
    * incremented every time a load notification is successfully handled -
@@ -196,14 +194,12 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * @param ssm
    * @param pdbentry
    * @param sequenceIs
-   * @param chains
    * @param protocol
    */
   public JalviewChimeraBinding(StructureSelectionManager ssm,
-          PDBEntry[] pdbentry, SequenceI[][] sequenceIs, String[][] chains,
-          String protocol)
+          PDBEntry[] pdbentry, SequenceI[][] sequenceIs, String protocol)
   {
-    super(ssm, pdbentry, sequenceIs, chains, protocol);
+    super(ssm, pdbentry, sequenceIs, protocol);
     viewer = new ChimeraManager(
             new ext.edu.ucsf.rbvi.strucviz2.StructureManager(true));
   }
@@ -253,11 +249,14 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     boolean first = true;
     for (String chain : toshow)
     {
+      int modelNumber = getModelNoForChain(chain);
+      String showChainCmd = modelNumber == -1 ? "" : modelNumber + ":."
+              + chain.split(":")[1];
       if (!first)
       {
         cmd.append(",");
       }
-      cmd.append(":.").append(chain);
+      cmd.append(showChainCmd);
       first = false;
     }
 
@@ -266,7 +265,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
      * window, but it looks more helpful not to (easier to relate chains to the
      * whole)
      */
-    final String command = "~display #*; ~ribbon #*; ribbon "
+    final String command = "~display #*; ~ribbon #*; ribbon :"
             + cmd.toString();
     sendChimeraCommand(command, false);
   }
@@ -287,7 +286,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       chimeraListener.shutdown();
       chimeraListener = null;
     }
-    lastCommand = null;
     viewer = null;
 
     releaseUIResources();
@@ -601,30 +599,42 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Send a command to Chimera, and optionally log any responses.
+   * Send a command to Chimera, and optionally log and return any responses.
+   * <p>
+   * Does nothing, and returns null, if the command is the same as the last one
+   * sent [why?].
    * 
    * @param command
-   * @param logResponse
+   * @param getResponse
    */
-  public void sendChimeraCommand(final String command, boolean logResponse)
+  public List<String> sendChimeraCommand(final String command,
+          boolean getResponse)
   {
     if (viewer == null)
     {
       // ? thread running after viewer shut down
-      return;
+      return null;
     }
+    List<String> reply = null;
     viewerCommandHistory(false);
-    if (lastCommand == null || !lastCommand.equals(command))
+    if (true /*lastCommand == null || !lastCommand.equals(command)*/)
     {
       // trim command or it may never find a match in the replyLog!!
-      lastReply = viewer.sendChimeraCommand(command.trim(), logResponse);
-      if (logResponse && debug)
+      List<String> lastReply = viewer.sendChimeraCommand(command.trim(),
+              getResponse);
+      if (getResponse)
       {
-        log("Response from command ('" + command + "') was:\n" + lastReply);
+        reply = lastReply;
+        if (debug)
+        {
+          log("Response from command ('" + command + "') was:\n"
+                  + lastReply);
+        }
       }
     }
     viewerCommandHistory(true);
-    lastCommand = command;
+
+    return reply;
   }
 
   /**
@@ -664,32 +674,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     }
     AlignmentI alignment = alignmentv.getAlignment();
 
-    for (jalview.structure.StructureMappingcommandSet cpdbbyseq : getColourBySequenceCommands(
-            files, sr, fr, alignment))
+    StructureMappingcommandSet colourBySequenceCommands = ChimeraCommands
+            .getColourBySequenceCommand(getSsm(), files, getSequence(), sr,
+                    fr, alignment);
+    for (String command : colourBySequenceCommands.commands)
     {
-      for (String command : cpdbbyseq.commands)
-      {
-        sendAsynchronousCommand(command, COLOURING_CHIMERA);
-      }
+      sendAsynchronousCommand(command, COLOURING_CHIMERA);
     }
   }
 
   /**
-   * @param files
-   * @param sr
-   * @param fr
-   * @param alignment
-   * @return
-   */
-  protected StructureMappingcommandSet[] getColourBySequenceCommands(
-          String[] files, SequenceRenderer sr, FeatureRenderer fr,
-          AlignmentI alignment)
-  {
-    return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
-            getSequence(), sr, fr, alignment);
-  }
-
-  /**
    * @param command
    */
   protected void executeWhenReady(String command)
@@ -715,17 +709,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   // End StructureListener
   // //////////////////////////
 
-  public Color getColour(int atomIndex, int pdbResNum, String chain,
-          String pdbfile)
-  {
-    if (getModelNum(pdbfile) < 0)
-    {
-      return null;
-    }
-    log("get model / residue colour attribute unimplemented");
-    return null;
-  }
-
   /**
    * returns the current featureRenderer that should be used to colour the
    * structures
@@ -744,30 +727,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    */
   public abstract void refreshPdbEntries();
 
-  private int getModelNum(String modelFileName)
-  {
-    String[] mfn = getPdbFile();
-    if (mfn == null)
-    {
-      return -1;
-    }
-    for (int i = 0; i < mfn.length; i++)
-    {
-      if (mfn[i].equalsIgnoreCase(modelFileName))
-      {
-        return i;
-      }
-    }
-    return -1;
-  }
-
-  /**
-   * map between index of model filename returned from getPdbFile and the first
-   * index of models from this file in the viewer. Note - this is not trimmed -
-   * use getPdbFile to get number of unique models.
-   */
-  private int _modelFileNameMap[];
-
   // ////////////////////////////////
   // /StructureListener
   @Override
@@ -777,33 +736,12 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     {
       return new String[0];
     }
-    // if (modelFileNames == null)
-    // {
-    // Collection<ChimeraModel> chimodels = viewer.getChimeraModels();
-    // _modelFileNameMap = new int[chimodels.size()];
-    // int j = 0;
-    // for (ChimeraModel chimodel : chimodels)
-    // {
-    // String mdlName = chimodel.getModelName();
-    // }
-    // modelFileNames = new String[j];
-    // // System.arraycopy(mset, 0, modelFileNames, 0, j);
-    // }
 
     return chimeraMaps.keySet().toArray(
             modelFileNames = new String[chimeraMaps.size()]);
   }
 
   /**
-   * map from string to applet
-   */
-  public Map getRegistryInfo()
-  {
-    // TODO Auto-generated method stub
-    return null;
-  }
-
-  /**
    * returns the current sequenceRenderer that should be used to colour the
    * structures
    * 
@@ -815,22 +753,22 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
           AlignmentViewPanel alignment);
 
   /**
-   * Construct and send a command to highlight zero, one or more atoms.
-   * 
-   * <pre>
-   * Done by generating a command like (to 'highlight' position 44)
-   *   show #0:44.C
-   * </pre>
+   * Construct and send a command to highlight zero, one or more atoms. We do
+   * this by sending an "rlabel" command to show the residue label at that
+   * position.
    */
   @Override
   public void highlightAtoms(List<AtomSpec> atoms)
   {
-    if (atoms == null)
+    if (atoms == null || atoms.size() == 0)
     {
       return;
     }
-    StringBuilder atomSpecs = new StringBuilder();
+
+    StringBuilder cmd = new StringBuilder(128);
     boolean first = true;
+    boolean found = false;
+
     for (AtomSpec atom : atoms)
     {
       int pdbResNum = atom.getPdbResNum();
@@ -839,39 +777,46 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
       if (cms != null && !cms.isEmpty())
       {
-        /*
-         * Formatting as #0:34.A,#1:33.A doesn't work as desired, so instead we
-         * concatenate multiple 'show' commands
-         */
-        atomSpecs.append(first ? "" : ";show ");
+        if (first)
+        {
+          cmd.append("rlabel #").append(cms.get(0).getModelNumber())
+                  .append(":");
+        }
+        else
+        {
+          cmd.append(",");
+        }
         first = false;
-        atomSpecs.append("#" + cms.get(0).getModelNumber());
-        atomSpecs.append(":" + pdbResNum);
+        cmd.append(pdbResNum);
         if (!chain.equals(" "))
         {
-          atomSpecs.append("." + chain);
+          cmd.append(".").append(chain);
         }
+        found = true;
       }
     }
-    String atomSpec = atomSpecs.toString();
+    String command = cmd.toString();
 
     /*
-     * Avoid repeated commands for the same residue
+     * avoid repeated commands for the same residue
      */
-    if (atomSpec.equals(lastMousedOverAtomSpec))
+    if (command.equals(lastHighlightCommand))
     {
       return;
     }
 
-    StringBuilder command = new StringBuilder(32);
-    viewerCommandHistory(false);
-    if (atomSpec.length() > 0)
+    /*
+     * unshow the label for the previous residue
+     */
+    if (lastHighlightCommand != null)
     {
-      command.append("show ").append(atomSpec);
-      viewer.sendChimeraCommand(command.toString(), false);
+      viewer.sendChimeraCommand("~" + lastHighlightCommand, false);
     }
-    viewerCommandHistory(true);
-    this.lastMousedOverAtomSpec = atomSpec;
+    if (found)
+    {
+      viewer.sendChimeraCommand(command, false);
+    }
+    this.lastHighlightCommand = command;
   }
 
   /**
@@ -888,59 +833,64 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
      * Parse model number, residue and chain for each selected position,
      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
      */
+    List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(selection);
+
+    /*
+     * Broadcast the selection (which may be empty, if the user just cleared all
+     * selections)
+     */
+    getSsm().mouseOverStructure(atomSpecs);
+  }
+
+  /**
+   * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
+   * corresponding residues (if any) in Jalview
+   * 
+   * @param structureSelection
+   * @return
+   */
+  protected List<AtomSpec> convertStructureResiduesToAlignment(
+          List<String> structureSelection)
+  {
     List<AtomSpec> atomSpecs = new ArrayList<AtomSpec>();
-    for (String atomSpec : selection)
+    for (String atomSpec : structureSelection)
     {
-      int colonPos = atomSpec.indexOf(":");
-      if (colonPos == -1)
-      {
-        continue; // malformed
-      }
-
-      int hashPos = atomSpec.indexOf("#");
-      String modelSubmodel = atomSpec.substring(hashPos + 1, colonPos);
-      int dotPos = modelSubmodel.indexOf(".");
-      int modelId = 0;
       try
       {
-        modelId = Integer.valueOf(dotPos == -1 ? modelSubmodel
-                : modelSubmodel.substring(0, dotPos));
-      } catch (NumberFormatException e)
+        AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec);
+        String pdbfilename = getPdbFileForModel(spec.getModelNumber());
+        spec.setPdbFile(pdbfilename);
+        atomSpecs.add(spec);
+      } catch (IllegalArgumentException e)
       {
-        // ignore, default to model 0
+        System.err.println("Failed to parse atomspec: " + atomSpec);
       }
+    }
+    return atomSpecs;
+  }
 
-      String residueChain = atomSpec.substring(colonPos + 1);
-      dotPos = residueChain.indexOf(".");
-      int pdbResNum = Integer.parseInt(dotPos == -1 ? residueChain
-              : residueChain.substring(0, dotPos));
-
-      String chainId = dotPos == -1 ? "" : residueChain
-              .substring(dotPos + 1);
-
-      /*
-       * Work out the pdbfilename from the model number
-       */
-      String pdbfilename = modelFileNames[frameNo];
-      findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
+  /**
+   * @param modelId
+   * @return
+   */
+  protected String getPdbFileForModel(int modelId)
+  {
+    /*
+     * Work out the pdbfilename from the model number
+     */
+    String pdbfilename = modelFileNames[0];
+    findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
+    {
+      for (ChimeraModel cm : chimeraMaps.get(pdbfile))
       {
-        for (ChimeraModel cm : chimeraMaps.get(pdbfile))
+        if (cm.getModelNumber() == modelId)
         {
-          if (cm.getModelNumber() == modelId)
-          {
-            pdbfilename = pdbfile;
-            break findfileloop;
-          }
+          pdbfilename = pdbfile;
+          break findfileloop;
         }
       }
-      atomSpecs.add(new AtomSpec(pdbfilename, chainId, pdbResNum, 0));
     }
-
-    /*
-     * Broadcast the selection (which may be empty, if the user just cleared all
-     * selections)
-     */
-    getSsm().mouseOverStructure(atomSpecs);
+    return pdbfilename;
   }
 
   private void log(String message)
@@ -994,6 +944,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    */
   public abstract void refreshGUI();
 
+  @Override
   public void setLoadingFromArchive(boolean loadingFromArchive)
   {
     this.loadingFromArchive = loadingFromArchive;
@@ -1004,6 +955,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * @return true if Chimeral is still restoring state or loading is still going
    *         on (see setFinsihedLoadingFromArchive)
    */
+  @Override
   public boolean isLoadingFromArchive()
   {
     return loadingFromArchive && !loadingFinished;
@@ -1015,6 +967,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * 
    * @param finishedLoading
    */
+  @Override
   public void setFinishedLoadingFromArchive(boolean finishedLoading)
   {
     loadingFinished = finishedLoading;
@@ -1085,34 +1038,273 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * 
    * @return
    */
+  @Override
   public List<String> getChainNames()
   {
-    List<String> names = new ArrayList<String>();
-    String[][] allNames = getChains();
-    if (allNames != null)
+    return chainNames;
+  }
+
+  /**
+   * Send a 'focus' command to Chimera to recentre the visible display
+   */
+  public void focusView()
+  {
+    sendChimeraCommand("focus", false);
+  }
+
+  /**
+   * Send a 'show' command for all atoms in the currently selected columns
+   * 
+   * TODO: pull up to abstract structure viewer interface
+   * 
+   * @param vp
+   */
+  public void highlightSelection(AlignmentViewPanel vp)
+  {
+    List<Integer> cols = vp.getAlignViewport().getColumnSelection()
+            .getSelected();
+    AlignmentI alignment = vp.getAlignment();
+    StructureSelectionManager sm = getSsm();
+    for (SequenceI seq : alignment.getSequences())
     {
-      for (String[] chainsForPdb : allNames)
+      /*
+       * convert selected columns into sequence positions
+       */
+      int[] positions = new int[cols.size()];
+      int i = 0;
+      for (Integer col : cols)
       {
-        if (chainsForPdb != null)
-        {
-          for (String chain : chainsForPdb)
-          {
-            if (chain != null && !names.contains(chain))
-            {
-              names.add(chain);
-            }
-          }
-        }
+        positions[i++] = seq.findPosition(col);
       }
+      sm.highlightStructure(this, seq, positions);
     }
-    return names;
   }
 
   /**
-   * Send a 'focus' command to Chimera to recentre the visible display
+   * Constructs and send commands to Chimera to set attributes on residues for
+   * features visible in Jalview
+   * 
+   * @param avp
    */
-  public void focusView()
+  public void sendFeaturesToViewer(AlignmentViewPanel avp)
   {
-    sendChimeraCommand("focus", false);
+    // TODO refactor as required to pull up to an interface
+    AlignmentI alignment = avp.getAlignment();
+    FeatureRenderer fr = getFeatureRenderer(avp);
+
+    /*
+     * fr is null if feature display is turned off
+     */
+    if (fr == null)
+    {
+      return;
+    }
+
+    String[] files = getPdbFile();
+    if (files == null)
+    {
+      return;
+    }
+
+    StructureMappingcommandSet commandSet = ChimeraCommands
+            .getSetAttributeCommandsForFeatures(getSsm(), files,
+                    getSequence(), fr, alignment);
+    String[] commands = commandSet.commands;
+    if (commands.length > 10)
+    {
+      sendCommandsByFile(commands);
+    }
+    else
+    {
+      for (String command : commands)
+      {
+        sendAsynchronousCommand(command, null);
+      }
+    }
+  }
+
+  /**
+   * Write commands to a temporary file, and send a command to Chimera to open
+   * the file as a commands script. For use when sending a large number of
+   * separate commands would overload the REST interface mechanism.
+   * 
+   * @param commands
+   */
+  protected void sendCommandsByFile(String[] commands)
+  {
+    try
+    {
+      File tmp = File.createTempFile("chim", ".com");
+      tmp.deleteOnExit();
+      PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
+      for (String command : commands)
+      {
+        out.println(command);
+      }
+      out.flush();
+      out.close();
+      String path = tmp.getAbsolutePath();
+      sendAsynchronousCommand("open cmd:" + path, null);
+    } catch (IOException e)
+    {
+      System.err
+              .println("Sending commands to Chimera via file failed with "
+                      + e.getMessage());
+    }
+  }
+
+  /**
+   * Get Chimera residues which have the named attribute, find the mapped
+   * positions in the Jalview sequence(s), and set as sequence features
+   * 
+   * @param attName
+   * @param alignmentPanel
+   */
+  public void copyStructureAttributesToFeatures(String attName,
+          AlignmentViewPanel alignmentPanel)
+  {
+    // todo pull up to AAStructureBindingModel (and interface?)
+
+    /*
+     * ask Chimera to list residues with the attribute, reporting its value
+     */
+    // this alternative command
+    // list residues spec ':*/attName' attr attName
+    // doesn't report 'None' values (which is good), but
+    // fails for 'average.bfactor' (which is bad):
+
+    String cmd = "list residues attr '" + attName + "'";
+    List<String> residues = sendChimeraCommand(cmd, true);
+
+    boolean featureAdded = createFeaturesForAttributes(attName, residues);
+    if (featureAdded)
+    {
+      alignmentPanel.getFeatureRenderer().featuresAdded();
+    }
+  }
+
+  /**
+   * Create features in Jalview for the given attribute name and structure
+   * residues.
+   * 
+   * <pre>
+   * The residue list should be 0, 1 or more reply lines of the format: 
+   *     residue id #0:5.A isHelix -155.000836316 index 5 
+   * or 
+   *     residue id #0:6.A isHelix None
+   * </pre>
+   * 
+   * @param attName
+   * @param residues
+   * @return
+   */
+  protected boolean createFeaturesForAttributes(String attName,
+          List<String> residues)
+  {
+    boolean featureAdded = false;
+    String featureGroup = getViewerFeatureGroup();
+
+    for (String residue : residues)
+    {
+      AtomSpec spec = null;
+      String[] tokens = residue.split(" ");
+      if (tokens.length < 5)
+      {
+        continue;
+      }
+      String atomSpec = tokens[2];
+      String attValue = tokens[4];
+
+      /*
+       * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
+       */
+      if ("None".equalsIgnoreCase(attValue)
+              || "False".equalsIgnoreCase(attValue))
+      {
+        continue;
+      }
+
+      try
+      {
+        spec = AtomSpec.fromChimeraAtomspec(atomSpec);
+      } catch (IllegalArgumentException e)
+      {
+        System.err.println("Problem parsing atomspec " + atomSpec);
+        continue;
+      }
+
+      String chainId = spec.getChain();
+      String description = attValue;
+      float score = Float.NaN;
+      try
+      {
+        score = Float.valueOf(attValue);
+        description = chainId;
+      } catch (NumberFormatException e)
+      {
+        // was not a float value
+      }
+
+      String pdbFile = getPdbFileForModel(spec.getModelNumber());
+      spec.setPdbFile(pdbFile);
+
+      List<AtomSpec> atoms = Collections.singletonList(spec);
+
+      /*
+       * locate the mapped position in the alignment (if any)
+       */
+      SearchResults sr = getSsm()
+              .findAlignmentPositionsForStructurePositions(atoms);
+
+      /*
+       * expect one matched alignment position, or none 
+       * (if the structure position is not mapped)
+       */
+      for (SearchResultMatchI m : sr.getResults())
+      {
+        SequenceI seq = m.getSequence();
+        int start = m.getStart();
+        int end = m.getEnd();
+        SequenceFeature sf = new SequenceFeature(attName, description,
+                start, end, score, featureGroup);
+        // todo: should SequenceFeature have an explicit property for chain?
+        // note: repeating the action shouldn't duplicate features
+        featureAdded |= seq.addSequenceFeature(sf);
+      }
+    }
+    return featureAdded;
+  }
+
+  /**
+   * Answers the feature group name to apply to features created in Jalview from
+   * Chimera attributes
+   * 
+   * @return
+   */
+  protected String getViewerFeatureGroup()
+  {
+    // todo pull up to interface
+    return CHIMERA_FEATURE_GROUP;
+  }
+
+
+  public Hashtable<String, String> getChainFile()
+  {
+    return chainFile;
+  }
+
+  public List<ChimeraModel> getChimeraModelByChain(String chain)
+  {
+    return chimeraMaps.get(chainFile.get(chain));
+  }
+
+  public int getModelNoForChain(String chain)
+  {
+    List<ChimeraModel> foundModels = getChimeraModelByChain(chain);
+    if (foundModels != null && !foundModels.isEmpty())
+    {
+      return foundModels.get(0).getModelNumber();
+    }
+    return -1;
   }
 }