JAL-1333 Chimera specific code extracted from the StructureViz library for opening...
authorJim Procter <jprocter@Jims-MacBook-Pro-2.local>
Wed, 2 Jul 2014 22:31:47 +0000 (15:31 -0700)
committerJim Procter <jprocter@Jims-MacBook-Pro-2.local>
Wed, 2 Jul 2014 22:31:47 +0000 (15:31 -0700)
THIRDPARTYLIBS
src/ext/edu/ucsf/rbvi/strucviz2/ChimUtils.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraChain.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraModel.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraResidue.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraStructuralObject.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraTreeModel.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/StructureSettings.java [new file with mode: 0644]
src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java [new file with mode: 0644]

index 179e679..386541e 100644 (file)
@@ -2,6 +2,11 @@ The Jalview Desktop relies on a number of third-party libraries.
 These can be found in the lib directory, with additional compile
 time dependencies in the utils directory.
 
+A number of sources have also been adapted for incorporation into Jalview's source tree
+
+ext.edu.ucsf.rbvi.strucviz2 includes sources originally developed by Scooter Morris and Nadezhda Doncheva for the Cytoscape StructureViz2 plugin. It is released under the Berkley license and we hereby acknowledge its original copyright is held by the UCSF Computer Graphics Laboratory
+ and the software was developed with support by the NIH National Center for Research Resources, grant P41-RR01081. 
+
 Licencing information for each library is given below:
 
 JGoogleAnalytics_0.3.jar       APL 2.0 License - http://code.google.com/p/jgoogleanalytics/
@@ -34,6 +39,7 @@ wsdl4j.jar
 xercesImpl.jar
 xml-apis.jar
 json_simple-1.1.jar : Apache 2.0 license : downloaded from https://code.google.com/p/json-simple/downloads/list (will move to 1.1.1 version when jalview is mavenised and osgi-ised)
+
 Additional dependencies
 
 examples/javascript/deployJava.js : http://java.com/js/deployJava.js
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimUtils.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimUtils.java
new file mode 100644 (file)
index 0000000..aedf657
--- /dev/null
@@ -0,0 +1,755 @@
+package ext.edu.ucsf.rbvi.strucviz2;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
+
+
+public abstract class ChimUtils {
+
+       private static Logger logger = LoggerFactory
+                       .getLogger(ChimUtils.class);
+
+       static int MAX_SUB_MODELS = 1000;
+
+       public static final HashMap<String, String> aaNames;
+
+       public static String RESIDUE_ATTR = "ChimeraResidue";
+       public static String RINALYZER_ATTR = "RINalyzerResidue";
+       public static String DEFAULT_STRUCTURE_KEY = "pdbFileName";
+
+       /**
+        * Parse the model number returned by Chimera and return the int value
+        */
+       // invoked by the ChimeraModel constructor
+       // line = model id #0 type Molecule name 1ert
+       public static int[] parseModelNumber(String inputLine) {
+               int hash = inputLine.indexOf('#');
+               int space = inputLine.indexOf(' ', hash);
+               int decimal = inputLine.substring(hash + 1, space).indexOf('.');
+               // model number is between hash+1 and space
+               int modelNumber = -1;
+               int subModelNumber = 0;
+               try {
+                       if (decimal > 0) {
+                               subModelNumber = Integer.parseInt(inputLine.substring(decimal + hash + 2, space));
+                               space = decimal + hash + 1;
+                       }
+                       modelNumber = Integer.parseInt(inputLine.substring(hash + 1, space));
+               } catch (Exception e) {
+                       logger.warn("Unexpected return from Chimera: " + inputLine, e);
+               }
+               return new int[] { modelNumber, subModelNumber };
+       }
+
+       /**
+        * Parse the model number returned by Chimera and return the int value
+        */
+       // invoked by openModel in ChimeraManager
+       // line: #1, chain A: hiv-1 protease
+       public static int[] parseOpenedModelNumber(String inputLine) {
+               int hash = inputLine.indexOf('#');
+               int space = inputLine.indexOf(',', hash);
+               int decimal = inputLine.substring(hash + 1, space).indexOf('.');
+               // model number is between hash+1 and space
+               int modelNumber = -1;
+               int subModelNumber = 0;
+               try {
+                       if (decimal > 0) {
+                               subModelNumber = Integer.parseInt(inputLine.substring(decimal + hash + 2, space));
+                               space = decimal + hash + 1;
+                       }
+                       modelNumber = Integer.parseInt(inputLine.substring(hash + 1, space));
+               } catch (Exception e) {
+                       logger.warn("Unexpected return from Chimera: " + inputLine, e);
+               }
+               return new int[] { modelNumber, subModelNumber };
+       }
+
+       /**
+        * Parse the model identifier returned by Chimera and return the String value
+        */
+       // invoked by the ChimeraModel constructor
+       // line = model id #0 type Molecule name 1ert
+       public static String parseModelName(String inputLine) {
+               int start = inputLine.indexOf("name ");
+               if (start < 0)
+                       return null;
+               // Might get a quoted string (don't understand why, but there you have it)
+               if (inputLine.startsWith("\"", start + 5)) {
+                       start += 6; // Skip over the first quote
+                       int end = inputLine.lastIndexOf('"');
+                       if (end >= 1) {
+                               return inputLine.substring(start, end);
+                       } else
+                               return inputLine.substring(start);
+               } else {
+                       return inputLine.substring(start + 5);
+               }
+       }
+
+       public static Color parseModelColor(String inputLine) {
+               try {
+                       int colorStart = inputLine.indexOf("color ");
+                       String colorString = inputLine.substring(colorStart + 6);
+                       String[] rgbStrings = colorString.split(",");
+                       float[] rgbValues = new float[4];
+                       for (int i = 0; i < rgbStrings.length; i++) {
+                               Float f = new Float(rgbStrings[i]);
+                               rgbValues[i] = f.floatValue();
+                       }
+                       if (rgbStrings.length == 4) {
+                               return new Color(rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3]);
+                       } else {
+                               return new Color(rgbValues[0], rgbValues[1], rgbValues[2]);
+                       }
+               } catch (Exception ex) {
+                       logger.warn("Unexpected return from Chimera: " + inputLine, ex);
+               }
+               return Color.white;
+       }
+
+       /**
+        * Create the key to use for forming the model/submodel key into the modelHash
+        * 
+        * @param model
+        *            the model number
+        * @param subModel
+        *            the submodel number
+        * @return the model key as an Integer
+        */
+       public static Integer makeModelKey(int model, int subModel) {
+               return new Integer(model * MAX_SUB_MODELS + subModel);
+       }
+
+       // invoked by the getResdiue (parseConnectivityReplies in CreateStructureNetworkTask)
+       // atomSpec = #0:1.A or #1:96.B@N
+       public static ChimeraModel getModel(String atomSpec, ChimeraManager chimeraManager) {
+               // System.out.println("getting model for "+atomSpec);
+               String[] split = atomSpec.split(":");
+               // No model specified....
+               if (split[0].length() == 0) {
+                       logger.info("Unexpected return from Chimera: " + atomSpec);
+                       return null;
+               }
+               // System.out.println("model = "+split[0].substring(1));
+               int model = 0;
+               int submodel = 0;
+               try {
+                       String[] subSplit = split[0].substring(1).split("\\.");
+                       if (subSplit.length > 0)
+                               model = Integer.parseInt(subSplit[0]);
+                       else
+                               model = Integer.parseInt(split[0].substring(1));
+
+                       if (subSplit.length > 1)
+                               submodel = Integer.parseInt(subSplit[1]);
+               } catch (Exception e) {
+                       // ignore
+                       logger.warn("Unexpected return from Chimera: " + atomSpec, e);
+               }
+               return chimeraManager.getChimeraModel(model, submodel);
+       }
+
+       // invoked by the parseConnectivityReplies in CreateStructureNetworkTask
+       // atomSpec = #0:1.A or #1:96.B@N
+       public static ChimeraResidue getResidue(String atomSpec, ChimeraManager chimeraManager) {
+               // System.out.println("Getting residue from: "+atomSpec);
+               ChimeraModel model = getModel(atomSpec, chimeraManager); // Get the model
+               if (model == null) {
+                       model = chimeraManager.getChimeraModel();
+               }
+               return getResidue(atomSpec, model);
+       }
+
+       // invoked by the getResdiue (parseConnectivityReplies in CreateStructureNetworkTask)
+       // atomSpec = #0:1.A or #1:96.B@N
+       public static ChimeraResidue getResidue(String atomSpec, ChimeraModel model) {
+               // System.out.println("Getting residue from: "+atomSpec);
+               String[] split = atomSpec.split(":|@");
+
+               // Split into residue and chain
+               String[] residueChain = split[1].split("\\.");
+
+               if (residueChain[0].length() == 0) {
+                       logger.info("Unexpected return from Chimera: " + atomSpec);
+                       return null;
+               }
+
+               if (residueChain.length == 2 && residueChain[1].length() > 0) {
+                       ChimeraChain chain = model.getChain(residueChain[1]);
+                       return chain.getResidue(residueChain[0]);
+               }
+               return model.getResidue("_", residueChain[0]);
+       }
+
+       public static ChimeraChain getChain(String atomSpec, ChimeraModel model) {
+               String[] split = atomSpec.split(":|@");
+
+               // Split into residue and chain
+               String[] residueChain = split[1].split("\\.");
+               if (residueChain.length == 1) {
+                       logger.info("Unexpected return from Chimera: " + atomSpec);
+                       return null;
+               }
+               return model.getChain(residueChain[1]);
+       }
+
+       public static String getAtomName(String atomSpec) {
+               String[] split = atomSpec.split("@");
+               if (split.length > 1) {
+                       return split[1];
+               }
+               return atomSpec;
+       }
+
+       public static boolean isBackbone(String atom) {
+               if (atom.equals("C") || atom.equals("CA") || atom.equals("N") || atom.equals("O")
+                               || atom.equals("H"))
+                       return true;
+               return false;
+       }
+
+       public static String getIntSubtype(String node, String atom) {
+               String[] split = node.split("#| ");
+               String resType = "";
+               if (split.length == 2) {
+                       resType = split[0].trim().toUpperCase();
+               } else if (split.length == 3) {
+                       resType = split[1].trim().toUpperCase();
+               }
+               if (resType.equalsIgnoreCase("HOH") || resType.equalsIgnoreCase("WAT")) {
+                       return "water";
+               } else if (aaNames.containsKey(resType)) {
+                       if (atom.equals("C") || atom.equals("CA") || atom.equals("N") || atom.equals("O")
+                                       || atom.equals("H")) {
+                               return "mc";
+                       } else {
+                               return "sc";
+                       }
+               } else {
+                       return "other";
+               }
+       }
+
+
+       public static String[] getResKeyParts(String resKey) {
+               // [pdbID[.modelNo]#][residueID][.chainID]
+               // pdbID := 4-character code | "URL" | "path"
+               String[] resKeyParts = new String[4];
+               String[] split = resKey.split("#");
+               String resChain = null;
+               // if no "#" then it is either only a pdb id or a residue or a chain
+               if (split.length == 1) {
+                       // pdb id without model
+                       if (resKey.length() == 4 && resKey.indexOf("\\.") < 0) {
+                               parseModelID(resKey, resKeyParts);
+                       }
+                       // pdb link or file
+                       else if (resKey.startsWith("\"")) {
+                               parseModelID(resKey, resKeyParts);
+                       }
+                       // chain and residue or model and number
+                       else {
+                               String[] splitSplit = resKey.split("\\.");
+                               if (splitSplit.length == 1) {
+                                       // only a chain or a residue
+                                       resChain = resKey;
+                               } else {
+                                       try {
+                                               // pdb with a model
+                                               Integer.parseInt(splitSplit[1]);
+                                               parseModelID(resKey, resKeyParts);
+                                       } catch (NumberFormatException ex) {
+                                               // residue and chain
+                                               resChain = resKey;
+                                       }
+                               }
+                       }
+               } else if (split.length == 2) {
+                       // model and residue+chain
+                       parseModelID(split[0], resKeyParts);
+                       resChain = split[1];
+               } else {
+                       // model string with "#"
+                       // TODO: [Optional] Are there more possibilities?
+                       parseModelID(resKey.substring(0, resKey.lastIndexOf("#")), resKeyParts);
+                       resChain = resKey.substring(resKey.lastIndexOf("#") + 1, resKey.length());
+               }
+               if (resChain != null) {
+                       //System.out.println(resChain);
+                       String[] resChainSplit = resChain.split("\\.");
+                       if (resChainSplit.length == 1) {
+                               // TODO: [Optional] Find a better way to distinguish between chain and residue
+                               // if only one character and not an int, probably a chain
+                               if (resChainSplit[0].length() == 1) {
+                                       try {
+                                               Integer.parseInt(resChainSplit[0]);
+                                               resKeyParts[3] = resChainSplit[0];
+                                       } catch (NumberFormatException ex) {
+                                               resKeyParts[2] = resChainSplit[0];
+                                       }
+                               } else {
+                                       resKeyParts[3] = resChainSplit[0];
+                               }
+                       } else if (resChainSplit.length == 2) {
+                               resKeyParts[2] = resChainSplit[0];
+                               resKeyParts[3] = resChainSplit[1];
+                       } else {
+                               // too many dots?
+                               logger.info("Could not parse residue identifier: " + resKey);
+                       }
+               }
+               // String print = "";
+               // for (int i = 0; i < resKeyParts.length; i++) {
+               // if (resKeyParts[i] == null) {
+               // print += i + ": null\t";
+               // } else {
+               // print += i + ": " + resKeyParts[i] + ";";
+               // }
+               // }
+               // System.out.println(print);
+               return resKeyParts;
+       }
+
+       public static void parseModelID(String modelID, String[] resKeyParts) {
+               if (modelID.startsWith("\"")) {
+                       if (modelID.endsWith("\"")) {
+                               resKeyParts[0] = modelID.substring(1, modelID.length() - 1);
+                               return;
+                       } else {
+                               try {
+                                       Integer.parseInt(modelID.substring(modelID.lastIndexOf("\"") + 2,
+                                                       modelID.length()));
+                                       resKeyParts[0] = modelID.substring(0, modelID.lastIndexOf("\"") - 1);
+                                       resKeyParts[1] = modelID.substring(modelID.lastIndexOf("\"") + 2,
+                                                       modelID.length());
+                               } catch (NumberFormatException ex) {
+                                       resKeyParts[0] = modelID.substring(1);
+                               }
+                       }
+               } else {
+                       String[] modelIDNo = modelID.split("\\.");
+                       if (modelIDNo.length == 1) {
+                               resKeyParts[0] = modelIDNo[0];
+                       } else if (modelIDNo.length == 2) {
+                               try {
+                                       Integer.parseInt(modelIDNo[1]);
+                                       resKeyParts[0] = modelIDNo[0];
+                                       resKeyParts[1] = modelIDNo[1];
+                               } catch (NumberFormatException ex) {
+                                       resKeyParts[0] = modelID;
+                               }
+                       } else {
+                               logger.info("Could not parse model identifier: " + modelID);
+                       }
+               }
+       }
+
+       /**
+        * This method takes a Cytoscape attribute specification ([structure#][residue][.chainID]) and
+        * returns the lowest-level object referenced by the spec. For example, if the spec is "1tkk",
+        * this method will return a ChimeraModel. If the spec is ".A", it will return a ChimeraChain,
+        * etc.
+        * 
+        * @param attrSpec
+        *            the specification string
+        * @param chimeraManager
+        *            the Chimera object we're currently using
+        * @return a ChimeraStructuralObject of the lowest type
+        */
+       public static ChimeraStructuralObject fromAttributeOld(String attrSpec,
+                       ChimeraManager chimeraManager) {
+               if (attrSpec == null || attrSpec.indexOf(',') > 0 || attrSpec.indexOf('-') > 0) {
+                       // No support for either lists or ranges
+                       logger.warn("No support for identifier: " + attrSpec);
+                       return null;
+               }
+
+               String residue = null;
+               String model = null;
+               String chain = null;
+
+               ChimeraModel chimeraModel = null;
+               ChimeraChain chimeraChain = null;
+               ChimeraResidue chimeraResidue = null;
+
+               // System.out.println("Getting object from attribute: "+attrSpec);
+               try {
+                       String[] split = attrSpec.split("#");
+                       String resChain = null;
+                       if (split.length == 1) {
+                               // no model
+                               resChain = split[0];
+                       } else if (split.length == 2) {
+                               // model and rest
+                               model = split[0];
+                               resChain = split[1];
+                       } else {
+                               // model string with "#"
+                               model = attrSpec.substring(0, attrSpec.lastIndexOf("#"));
+                               resChain = attrSpec.substring(attrSpec.lastIndexOf("#") + 1, attrSpec.length());
+                       }
+                       if (resChain != null) {
+                               String[] resChainSplit = resChain.split("\\.");
+                               if (resChainSplit.length == 1) {
+                                       residue = resChainSplit[0];
+                               } else if (resChainSplit.length == 2) {
+                                       residue = resChainSplit[0];
+                                       chain = resChainSplit[1];
+                               } else {
+                                       // too many dots?
+                                       logger.warn("No support for identifier: " + attrSpec);
+                               }
+                       }
+
+                       // if (split.length == 1) {
+                       // // No model
+                       // residue = split[0];
+                       // } else if (split.length == 3) {
+                       // // We have all three
+                       // model = split[0];
+                       // residue = split[1];
+                       // chain = split[2];
+                       // } else if (split.length == 2 && attrSpec.indexOf('#') > 0) {
+                       // // Model and Residue
+                       // model = split[0];
+                       // residue = split[1];
+                       // } else {
+                       // // Residue and Chain
+                       // residue = split[0];
+                       // chain = split[1];
+                       // }
+
+                       // System.out.println("model = " + model + " chain = " + chain + " residue = " +
+                       // residue);
+                       if (model != null) {
+                               List<ChimeraModel> models = chimeraManager.getChimeraModels(model,
+                                               ModelType.PDB_MODEL);
+                               if (models.size() == 1) {
+                                       chimeraModel = models.get(0);
+                               } else {
+                                       try {
+                                               chimeraModel = chimeraManager.getChimeraModel(Integer.valueOf(model), 0);
+                                       } catch (NumberFormatException ex) {
+                                               // ignore
+                                       }
+                               }
+                       }
+                       if (chimeraModel == null) {
+                               chimeraModel = chimeraManager.getChimeraModel();
+                       }
+                       // System.out.println("ChimeraModel = " + chimeraModel);
+
+                       if (chain != null) {
+                               chimeraChain = chimeraModel.getChain(chain);
+                               // System.out.println("ChimeraChain = " + chimeraChain);
+                       }
+                       if (residue != null) {
+                               if (chimeraChain != null) {
+                                       chimeraResidue = chimeraChain.getResidue(residue);
+                               } else {
+                                       chimeraResidue = chimeraModel.getResidue("_", residue);
+                               }
+                               // System.out.println("ChimeraResidue = " + chimeraResidue);
+                       }
+
+                       if (chimeraResidue != null)
+                               return chimeraResidue;
+
+                       if (chimeraChain != null)
+                               return chimeraChain;
+
+                       if (chimeraModel != null)
+                               return chimeraModel;
+
+               } catch (Exception ex) {
+                       logger.warn("Could not parse residue identifier: " + attrSpec, ex);
+               }
+               return null;
+       }
+
+       public static ChimeraStructuralObject fromAttribute(String attrSpec,
+                       ChimeraManager chimeraManager) {
+               // TODO: Make sure it is OK to remove this: || attrSpec.indexOf('-') > 0
+               if (attrSpec == null || attrSpec.indexOf(',') > 0) {
+                       // No support for either lists or ranges
+                       // System.out.println("No support for identifier: " + attrSpec);
+                       logger.warn("No support for identifier: " + attrSpec);
+                       return null;
+               }
+               String[] modelIDNoResChain = getResKeyParts(attrSpec);
+
+               ChimeraModel chimeraModel = null;
+               ChimeraChain chimeraChain = null;
+               ChimeraResidue chimeraResidue = null;
+
+               // System.out.println("Getting object from attribute: "+attrSpec);
+               try {
+                       if (modelIDNoResChain[0] != null) {
+                               String modelID = modelIDNoResChain[0];
+                               List<ChimeraModel> models = chimeraManager.getChimeraModels(modelID,
+                                               ModelType.PDB_MODEL);
+                               if (models.size() == 1) { // usual case with only one model
+                                       chimeraModel = models.get(0);
+                               } else if (models.size() > 1 && modelIDNoResChain[1] != null) {
+                                       // there are several submodels
+                                       try {
+                                               int modelNo = Integer.valueOf(modelIDNoResChain[1]);
+                                               for (ChimeraModel model : models) {
+                                                       if (model.getSubModelNumber() == modelNo) {
+                                                               chimeraModel = model;
+                                                               break;
+                                                       }
+                                               }
+                                       } catch (NumberFormatException ex) {
+                                               // ignore
+                                       }
+                               } else {
+                                       // TODO: [Optional] What is this doing?
+                                       try {
+                                               chimeraModel = chimeraManager.getChimeraModel(Integer.valueOf(modelID), 0);
+                                       } catch (NumberFormatException ex) {
+                                               // ignore
+                                       }
+                               }
+                       }
+                       if (chimeraModel == null) {
+                               // TODO: [Optional] Find a better way to handle this case
+                               // If no model can be matched, continue
+                               // System.out.println("No matching model could be find for " + attrSpec);
+                               return null;
+                               // chimeraModel = chimeraManager.getChimeraModel();
+                               // logger.warn("No matching model could be find for " + attrSpec + ". Trying with "
+                               // + chimeraModel.toSpec());
+                       }
+                       // System.out.println("ChimeraModel = " + chimeraModel);
+
+                       if (modelIDNoResChain[3] != null) {
+                               chimeraChain = chimeraModel.getChain(modelIDNoResChain[3]);
+                               // System.out.println("ChimeraChain = " + chimeraChain);
+                       }
+                       if (modelIDNoResChain[2] != null) {
+                               String residue = modelIDNoResChain[2];
+                               if (chimeraChain != null) {
+                                       chimeraResidue = chimeraChain.getResidue(residue);
+                               } else if (chimeraModel.getChain("_") != null) {
+                                       chimeraResidue = chimeraModel.getResidue("_", residue);
+                               } else if (chimeraModel.getChainCount() == 1) {
+                                       chimeraResidue = chimeraModel.getResidue(chimeraModel.getChainNames()
+                                                       .iterator().next(), residue);
+                               }
+                               // System.out.println("ChimeraResidue = " + chimeraResidue);
+                       }
+
+                       if (chimeraResidue != null)
+                               return chimeraResidue;
+
+                       if (chimeraChain != null)
+                               return chimeraChain;
+
+                       if (chimeraModel != null)
+                               return chimeraModel;
+
+               } catch (Exception ex) {
+                       // System.out.println("Could not parse chimera identifier: " +
+                       // attrSpec+"("+ex.getMessage()+")");
+                       logger.warn("Could not parse chimera identifier: " + attrSpec, ex);
+               }
+               return null;
+       }
+
+       /**
+        * Search for structure references in the residue list
+        * 
+        * @param residueList
+        *            the list of residues
+        * @return a concatenated list of structures encoded in the list
+        */
+       public static String findStructures(String residueList) {
+               if (residueList == null)
+                       return null;
+               String[] residues = residueList.split(",");
+               Map<String, String> structureNameMap = new HashMap<String, String>();
+               for (int i = 0; i < residues.length; i++) {
+                       String[] components = residues[i].split("#");
+                       if (components.length > 1) {
+                               structureNameMap.put(components[0], components[1]);
+                       }
+               }
+               if (structureNameMap.isEmpty())
+                       return null;
+
+               String structure = null;
+               for (String struct : structureNameMap.keySet()) {
+                       if (structure == null)
+                               structure = new String();
+                       else
+                               structure = structure.concat(",");
+                       structure = structure.concat(struct);
+               }
+               return structure;
+       }
+
+       // invoked by openStructures in StructureManager
+       public static List<String> parseFuncRes(List<String> residueNames, String modelName) {
+               List<String> resRanges = new ArrayList<String>();
+               for (int i = 0; i < residueNames.size(); i++) {
+                       String residue = residueNames.get(i);
+                       // Parse out the structure, if there is one
+                       String[] components = residue.split("#");
+                       if (components.length > 1 && !modelName.equals(components[0])) {
+                               continue;
+                       } else if (components.length > 1) {
+                               residue = components[1];
+                       } else if (components.length == 1) {
+                               residue = components[0];
+                       }
+                       // Check to see if we have a range-spec
+                       String resRange = "";
+                       if (residue == null || residue.equals("") || residue.length() == 0) {
+                               continue;
+                       }
+                       String[] range = residue.split("-", 2);
+                       String chain = null;
+                       for (int res = 0; res < range.length; res++) {
+                               if (res == 1) {
+                                       resRange = resRange.concat("-");
+                                       if (chain != null && range[res].indexOf('.') == -1)
+                                               range[res] = range[res].concat("." + chain);
+                               }
+
+                               if (res == 0 && range.length >= 2 && range[res].indexOf('.') > 0) {
+                                       // This is a range spec with the leading residue containing a chain spec
+                                       String[] resChain = range[res].split("\\.");
+                                       chain = resChain[1];
+                                       range[res] = resChain[0];
+                               }
+                               // Fix weird SFLD syntax...
+                               if (range[res].indexOf('|') > 0 && Character.isDigit(range[res].charAt(0))) {
+                                       int offset = range[res].indexOf('|');
+                                       String str = range[res].substring(offset + 1) + range[res].substring(0, offset);
+                                       range[res] = str;
+                               }
+
+                               // Convert to legal atom-spec
+                               if (Character.isDigit(range[res].charAt(0))) {
+                                       resRange = resRange.concat(range[res]);
+                               } else if (Character.isDigit(range[res].charAt(1))) {
+                                       resRange = resRange.concat(range[res].substring(1));
+                               } else if (range[res].charAt(0) == '.') {
+                                       // Do we have a chain spec?
+                                       resRange = resRange.concat(range[res]);
+                               } else {
+                                       resRange = resRange.concat(range[res].substring(3));
+                               }
+                       }
+                       if (!resRanges.contains(resRange)) {
+                               resRanges.add(resRange);
+                       }
+               }
+               return resRanges;
+       }
+
+       static {
+               aaNames = new HashMap<String, String>();
+               aaNames.put("ALA", "A Ala Alanine N[C@@H](C)C(O)=O");
+               aaNames.put("ARG", "R Arg Arginine N[C@@H](CCCNC(N)=N)C(O)=O");
+               aaNames.put("ASN", "N Asn Asparagine N[C@@H](CC(N)=O)C(O)=O");
+               aaNames.put("ASP", "D Asp Aspartic_acid N[C@@H](CC(O)=O)C(O)=O");
+               aaNames.put("CYS", "C Cys Cysteine N[C@@H](CS)C(O)=O");
+               aaNames.put("GLN", "Q Gln Glutamine N[C@H](C(O)=O)CCC(N)=O");
+               aaNames.put("GLU", "E Glu Glumatic_acid N[C@H](C(O)=O)CCC(O)=O");
+               aaNames.put("GLY", "G Gly Glycine NCC(O)=O");
+               aaNames.put("HIS", "H His Histidine N[C@@H](CC1=CN=CN1)C(O)=O");
+               aaNames.put("ILE", "I Ile Isoleucine N[C@]([C@H](C)CC)([H])C(O)=O");
+               aaNames.put("LEU", "L Leu Leucine N[C@](CC(C)C)([H])C(O)=O");
+               aaNames.put("LYS", "K Lys Lysine N[C@](CCCCN)([H])C(O)=O");
+               aaNames.put("DLY", "K Dly D-Lysine NCCCC[C@@H](N)C(O)=O");
+               aaNames.put("MET", "M Met Methionine N[C@](CCSC)([H])C(O)=O");
+               aaNames.put("PHE", "F Phe Phenylalanine N[C@](CC1=CC=CC=C1)([H])C(O)=O");
+               aaNames.put("PRO", "P Pro Proline OC([C@@]1([H])NCCC1)=O");
+               aaNames.put("SER", "S Ser Serine OC[C@](C(O)=O)([H])N");
+               aaNames.put("THR", "T Thr Threonine O[C@H](C)[C@](C(O)=O)([H])N");
+               aaNames.put("TRP", "W Trp Tryptophan N[C@@]([H])(CC1=CN([H])C2=C1C=CC=C2)C(O)=O");
+               aaNames.put("TYR", "Y Tyr Tyrosine N[C@@](C(O)=O)([H])CC1=CC=C(O)C=C1");
+               aaNames.put("VAL", "V Val Valine N[C@@](C(O)=O)([H])C(C)C");
+               aaNames.put("ASX", "B Asx Aspartic_acid_or_Asparagine");
+               aaNames.put("GLX", "Z Glx Glutamine_or_Glutamic_acid");
+               aaNames.put("XAA", "X Xaa Any_or_unknown_amino_acid");
+               aaNames.put("HOH", "HOH HOH Water [H]O[H]");
+       }
+
+       /**
+        * Convert the amino acid type to a full name
+        * 
+        * @param aaType
+        *            the residue type to convert
+        * @return the full name of the residue
+        */
+       public static String toFullName(String aaType) {
+               if (!aaNames.containsKey(aaType))
+                       return aaType;
+               String[] ids = ((String) aaNames.get(aaType)).split(" ");
+               return ids[2].replace('_', ' ');
+       }
+
+       /**
+        * Convert the amino acid type to a single letter
+        * 
+        * @param aaType
+        *            the residue type to convert
+        * @return the single letter representation of the residue
+        */
+       public static String toSingleLetter(String aaType) {
+               if (!aaNames.containsKey(aaType))
+                       return aaType;
+               String[] ids = ((String) aaNames.get(aaType)).split(" ");
+               return ids[0];
+       }
+
+       /**
+        * Convert the amino acid type to three letters
+        * 
+        * @param aaType
+        *            the residue type to convert
+        * @return the three letter representation of the residue
+        */
+       public static String toThreeLetter(String aaType) {
+               if (!aaNames.containsKey(aaType))
+                       return aaType;
+               String[] ids = ((String) aaNames.get(aaType)).split(" ");
+               return ids[1];
+       }
+
+       /**
+        * Convert the amino acid type to its SMILES string
+        * 
+        * @param aaType
+        *            the residue type to convert
+        * @return the SMILES representation of the residue
+        */
+       public static String toSMILES(String aaType) {
+               if (!aaNames.containsKey(aaType))
+                       return null;
+               String[] ids = ((String) aaNames.get(aaType)).split(" ");
+               if (ids.length < 4)
+                       return null;
+               return ids[3];
+       }
+
+       public static String getAlignName(ChimeraStructuralObject chimObj) {
+               String name = chimObj.getChimeraModel().toString();
+               if (chimObj instanceof ChimeraChain) {
+                       name = ((ChimeraChain) chimObj).toString() + " [" + name + "]";
+               }
+               return name;
+       }
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraChain.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraChain.java
new file mode 100644 (file)
index 0000000..92193f2
--- /dev/null
@@ -0,0 +1,330 @@
+/* vim: set ts=2: */
+/**
+ * Copyright (c) 2006 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions, and the following disclaimer.
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions, and the following
+ *      disclaimer in the documentation and/or other materials provided
+ *      with the distribution.
+ *   3. Redistributions must acknowledge that this software was
+ *      originally developed by the UCSF Computer Graphics Laboratory
+ *      under support by the NIH National Center for Research Resources,
+ *      grant P41-RR01081.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package ext.edu.ucsf.rbvi.strucviz2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.TreeMap;
+
+/**
+ * This class provides the implementation for the ChimeraChain object
+ * 
+ * @author scooter
+ * 
+ */
+// TODO: [Optional] Implement toAttr() method
+public class ChimeraChain implements ChimeraStructuralObject {
+
+       /**
+        * The model/subModel number this chain is a part of
+        */
+       private int modelNumber;
+       private int subModelNumber;
+
+       /**
+        * A pointer to the model this chain is a part of
+        */
+       private ChimeraModel chimeraModel;
+
+       /**
+        * The chainID (from the PDB record)
+        */
+       private String chainId;
+
+       /**
+        * The residues that are part of this chain
+        */
+       private TreeMap<String, ChimeraResidue> residueMap;
+
+       /**
+        * userData to associate with this chain
+        */
+       private Object userData;
+
+       /**
+        * Flag to indicate the selection state
+        */
+       private boolean selected = false;
+
+       /**
+        * Constructor to create a new ChimeraChain
+        * 
+        * @param model
+        *          the model number this chain is part of
+        * @param subModel
+        *          the subModel number this chain is part of
+        * @param chainId
+        *          the chain ID for this chain
+        */
+       public ChimeraChain(int model, int subModel, String chainId) {
+               this.modelNumber = model;
+               this.subModelNumber = subModel;
+               this.chainId = chainId;
+               residueMap = new TreeMap<String, ChimeraResidue>();
+       }
+
+       /**
+        * set the selected state of this chain
+        * 
+        * @param selected
+        *          a boolean to set the selected state to
+        */
+       public void setSelected(boolean selected) {
+               this.selected = selected;
+       }
+
+       /**
+        * return the selected state of this chain
+        * 
+        * @return the selected state
+        */
+       public boolean isSelected() {
+               return selected;
+       }
+
+       public boolean hasSelectedChildren() {
+               if (selected) {
+                       return true;
+               } else {
+                       for (ChimeraResidue residue : getResidues()) {
+                               if (residue.isSelected())
+                                       return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Return the list of selected residues
+        * 
+        * @return all selected residues
+        */
+       public List<ChimeraResidue> getSelectedResidues() {
+               List<ChimeraResidue> residueList = new ArrayList<ChimeraResidue>();
+               if (selected) {
+                       residueList.addAll(getResidues());
+               } else {
+                       for (ChimeraResidue residue : getResidues()) {
+                               if (residue.isSelected())
+                                       residueList.add(residue);
+                       }
+               }
+               return residueList;
+       }
+
+       /**
+        * Add a residue to the chain.
+        * 
+        * @param residue
+        *          the ChimeraResidue to add to the chain.
+        */
+       public void addResidue(ChimeraResidue residue) {
+               String index = residue.getIndex();
+               // Put it in our map so that we can return it in order
+               residueMap.put(index, residue);
+       }
+
+       /**
+        * Return the list of residues in this chain in pdb residue order
+        * 
+        * @return a Collection of residues in residue order
+        */
+       public Collection<ChimeraResidue> getResidues() {
+               return residueMap.values();
+       }
+
+       /**
+        * Return the list of residues in this chain as a list
+        * 
+        * @return List of residues
+        */
+       public List<ChimeraStructuralObject> getChildren() {
+               return new ArrayList<ChimeraStructuralObject>(residueMap.values());
+       }
+
+       /**
+        * Get a specific residue
+        * 
+        * @param residueIndex
+        *          String representation of the residue index
+        * @return the ChimeraResidue represented by the residueIndex
+        */
+       public ChimeraResidue getResidue(String index) {
+               // Integer index = new Integer(residueIndex);
+               if (residueMap.containsKey(index))
+                       return residueMap.get(index);
+               return null;
+       }
+
+       /**
+        * Get a list of residues as a residue range
+        * 
+        * @param residueRange
+        *          String representation of the residue range
+        * @return the List of ChimeraResidues represented by the range
+        */
+       public List<ChimeraResidue> getResidueRange(String residueRange) {
+               String[] range = residueRange.split("-", 2);
+               if (range[1] == null || range[1].length() == 0) {
+                       range[1] = range[0];
+               }
+               List<ChimeraResidue> resultRange = new ArrayList<ChimeraResidue>();
+               int start = Integer.parseInt(range[0]);
+               int end = Integer.parseInt(range[1]);
+               for (int i = start; i <= end; i++) {
+                       String index = String.valueOf(i);
+                       if (residueMap.containsKey(index))
+                               resultRange.add(residueMap.get(index));
+               }
+               return resultRange;
+       }
+
+       /**
+        * Get the ID for this chain
+        * 
+        * @return String value of the chainId
+        */
+       public String getChainId() {
+               return chainId;
+       }
+
+       /**
+        * Get the model number for this chain
+        * 
+        * @return the model number
+        */
+       public int getModelNumber() {
+               return modelNumber;
+       }
+
+       /**
+        * Get the sub-model number for this chain
+        * 
+        * @return the sub-model number
+        */
+       public int getSubModelNumber() {
+               return subModelNumber;
+       }
+
+       /**
+        * Return a string representation of this chain as follows: Chain <i>chainId</i>
+        * (<i>residue_count</i> residues)
+        * 
+        * @return String representation of chain
+        */
+       public String displayName() {
+               if (chainId.equals("_")) {
+                       return ("Chain (no ID) (" + getResidueCount() + " residues)");
+               } else {
+                       return ("Chain " + chainId + " (" + getResidueCount() + " residues)");
+               }
+       }
+
+       /**
+        * Return a string representation of this chain as follows: Node xxx [Model yyyy Chain
+        * <i>chainId</i>]
+        * 
+        * @return String representation of chain
+        */
+       public String toString() {
+               String displayName = chimeraModel.getModelName();
+               if (displayName.length() > 14)
+                       displayName = displayName.substring(0, 13) + "...";
+               if (chainId.equals("_")) {
+                       return (displayName + " Chain (no ID) (" + getResidueCount() + " residues)");
+               } else {
+                       return (displayName + " Chain " + chainId + " (" + getResidueCount() + " residues)");
+               }
+       }
+
+       /**
+        * Return the Chimera specification for this chain
+        * 
+        * @return Chimera specification
+        */
+       public String toSpec() {
+               if (chainId.equals("_")) {
+                       return ("#" + modelNumber + "." + subModelNumber + ":.");
+               } else {
+                       return ("#" + modelNumber + "." + subModelNumber + ":." + chainId);
+               }
+       }
+
+       /**
+        * Return the number of residues in this chain
+        * 
+        * @return integer number of residues
+        */
+       public int getResidueCount() {
+               return residueMap.size();
+       }
+
+       /**
+        * Set the ChimeraModel for this chain
+        * 
+        * @param model
+        *          ChimeraModel to associate with this chain
+        */
+       public void setChimeraModel(ChimeraModel model) {
+               this.chimeraModel = model;
+       }
+
+       /**
+        * Get the ChimeraModel for this chain
+        * 
+        * @return ChimeraModel associated with this chain
+        */
+       public ChimeraModel getChimeraModel() {
+               return chimeraModel;
+       }
+
+       /**
+        * Get the user data for this Chain
+        * 
+        * @return user data
+        */
+       public Object getUserData() {
+               return userData;
+       }
+
+       /**
+        * Set the user data for this Chain
+        * 
+        * @param data
+        *          the user data to associate with this chain
+        */
+       public void setUserData(Object data) {
+               this.userData = data;
+       }
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
new file mode 100644 (file)
index 0000000..d0bb84e
--- /dev/null
@@ -0,0 +1,673 @@
+package ext.edu.ucsf.rbvi.strucviz2;
+
+import java.awt.Color;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
+import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
+
+/**
+ * This object maintains the Chimera communication information.
+ */
+public class ChimeraManager
+{
+
+  static private Process chimera;
+
+  static private ListenerThreads chimeraListenerThreads;
+
+  static private Map<Integer, ChimeraModel> currentModelsMap;
+
+  private static Logger logger = LoggerFactory
+          .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
+
+  private StructureManager structureManager;
+
+  public ChimeraManager(StructureManager structureManager)
+  {
+    this.structureManager = structureManager;
+    chimera = null;
+    chimeraListenerThreads = null;
+    currentModelsMap = new HashMap<Integer, ChimeraModel>();
+    
+  }
+
+  public List<ChimeraModel> getChimeraModels(String modelName)
+  {
+    List<ChimeraModel> models = getChimeraModels(modelName,
+            ModelType.PDB_MODEL);
+    models.addAll(getChimeraModels(modelName, ModelType.SMILES));
+    return models;
+  }
+
+  public List<ChimeraModel> getChimeraModels(String modelName,
+          ModelType modelType)
+  {
+    List<ChimeraModel> models = new ArrayList<ChimeraModel>();
+    for (ChimeraModel model : currentModelsMap.values())
+    {
+      if (modelName.equals(model.getModelName())
+              && modelType.equals(model.getModelType()))
+      {
+        models.add(model);
+      }
+    }
+    return models;
+  }
+
+  public Map<String, List<ChimeraModel>> getChimeraModelsMap()
+  {
+    Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
+    for (ChimeraModel model : currentModelsMap.values())
+    {
+      String modelName = model.getModelName();
+      if (!models.containsKey(modelName))
+      {
+        models.put(modelName, new ArrayList<ChimeraModel>());
+      }
+      if (!models.get(modelName).contains(model))
+      {
+        models.get(modelName).add(model);
+      }
+    }
+    return models;
+  }
+
+  public ChimeraModel getChimeraModel(Integer modelNumber,
+          Integer subModelNumber)
+  {
+    Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
+    if (currentModelsMap.containsKey(key))
+    {
+      return currentModelsMap.get(key);
+    }
+    return null;
+  }
+
+  public ChimeraModel getChimeraModel()
+  {
+    return currentModelsMap.values().iterator().next();
+  }
+
+  public Collection<ChimeraModel> getChimeraModels()
+  {
+    // this method is invoked by the model navigator dialog
+    return currentModelsMap.values();
+  }
+
+  public int getChimeraModelsCount(boolean smiles)
+  {
+    // this method is invokes by the model navigator dialog
+    int counter = currentModelsMap.size();
+    if (smiles)
+    {
+      return counter;
+    }
+
+    for (ChimeraModel model : currentModelsMap.values())
+    {
+      if (model.getModelType() == ModelType.SMILES)
+      {
+        counter--;
+      }
+    }
+    return counter;
+  }
+
+  public boolean hasChimeraModel(Integer modelNubmer)
+  {
+    return hasChimeraModel(modelNubmer, 0);
+  }
+
+  public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
+  {
+    return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
+            subModelNumber));
+  }
+
+  public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
+          ChimeraModel model)
+  {
+    currentModelsMap.put(
+            ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
+  }
+
+  public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
+  {
+    int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
+    if (currentModelsMap.containsKey(modelKey))
+    {
+      currentModelsMap.remove(modelKey);
+    }
+  }
+
+  public List<ChimeraModel> openModel(String modelPath, ModelType type)
+  {
+    logger.info("chimera open " + modelPath);
+    stopListening();
+    List<String> response = null;
+    // TODO: [Optional] Handle modbase models
+    if (type == ModelType.MODBASE_MODEL)
+    {
+      response = sendChimeraCommand("open modbase:" + modelPath, true);
+      // } else if (type == ModelType.SMILES) {
+      // response = sendChimeraCommand("open smiles:" + modelName, true);
+      // modelName = "smiles:" + modelName;
+    }
+    else
+    {
+      response = sendChimeraCommand("open " + modelPath, true);
+    }
+    if (response == null)
+    {
+      // something went wrong
+      logger.warn("Could not open " + modelPath);
+      return null;
+    }
+    List<ChimeraModel> models = new ArrayList<ChimeraModel>();
+    int[] modelNumbers = null;
+    if (type == ModelType.PDB_MODEL)
+    {
+      for (String line : response)
+      {
+        if (line.startsWith("#"))
+        {
+          modelNumbers = ChimUtils.parseOpenedModelNumber(line);
+          if (modelNumbers != null)
+          {
+            int modelNumber = ChimUtils.makeModelKey(modelNumbers[0],
+                    modelNumbers[1]);
+            if (currentModelsMap.containsKey(modelNumber))
+            {
+              continue;
+            }
+            String modelName = modelPath;
+            // TODO: [Optional] Convert path to name in a better way
+            if (modelPath.lastIndexOf(File.separator) > 0)
+            {
+              modelName = modelPath.substring(modelPath
+                      .lastIndexOf(File.separator) + 1);
+            }
+            else if (modelPath.lastIndexOf("/") > 0)
+            {
+              modelName = modelPath
+                      .substring(modelPath.lastIndexOf("/") + 1);
+            }
+            ChimeraModel newModel = new ChimeraModel(modelName, type,
+                    modelNumbers[0], modelNumbers[1]);
+            currentModelsMap.put(modelNumber, newModel);
+            models.add(newModel);
+            modelNumbers = null;
+          }
+        }
+      }
+    }
+    else
+    {
+      // TODO: [Optional] Open smiles from file would fail. Do we need it?
+      // If parsing fails, iterate over all open models to get the right one
+      List<ChimeraModel> openModels = getModelList();
+      for (ChimeraModel openModel : openModels)
+      {
+        String openModelName = openModel.getModelName();
+        if (openModelName.endsWith("..."))
+        {
+          openModelName = openModelName.substring(0,
+                  openModelName.length() - 3);
+        }
+        if (modelPath.startsWith(openModelName))
+        {
+          openModel.setModelName(modelPath);
+          int modelNumber = ChimUtils
+                  .makeModelKey(openModel.getModelNumber(),
+                          openModel.getSubModelNumber());
+          if (!currentModelsMap.containsKey(modelNumber))
+          {
+            currentModelsMap.put(modelNumber, openModel);
+            models.add(openModel);
+          }
+        }
+      }
+    }
+
+    // assign color and residues to open models
+    for (ChimeraModel newModel : models)
+    {
+      // get model color
+      Color modelColor = getModelColor(newModel);
+      if (modelColor != null)
+      {
+        newModel.setModelColor(modelColor);
+      }
+
+      // Get our properties (default color scheme, etc.)
+      // Make the molecule look decent
+      // chimeraSend("repr stick "+newModel.toSpec());
+
+      // Create the information we need for the navigator
+      if (type != ModelType.SMILES)
+      {
+        addResidues(newModel);
+      }
+    }
+
+    sendChimeraCommand("focus", false);
+    startListening();
+    return models;
+  }
+
+  public void closeModel(ChimeraModel model)
+  {
+    // int model = structure.modelNumber();
+    // int subModel = structure.subModelNumber();
+    // Integer modelKey = makeModelKey(model, subModel);
+    stopListening();
+    logger.info("chimera close model " + model.getModelName());
+    if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
+            model.getModelNumber(), model.getSubModelNumber())))
+    {
+      sendChimeraCommand("close " + model.toSpec(), false);
+      // currentModelNamesMap.remove(model.getModelName());
+      currentModelsMap.remove(ChimUtils.makeModelKey(
+              model.getModelNumber(), model.getSubModelNumber()));
+      // selectionList.remove(chimeraModel);
+    }
+    else
+    {
+      logger.warn("Could not find model " + model.getModelName()
+              + " to close.");
+    }
+    startListening();
+  }
+
+  public void startListening()
+  {
+    sendChimeraCommand("listen start models; listen start select", false);
+  }
+
+  public void stopListening()
+  {
+    sendChimeraCommand("listen stop models; listen stop select", false);
+  }
+
+  /**
+   * Select something in Chimera
+   * 
+   * @param command
+   *          the selection command to pass to Chimera
+   */
+  public void select(String command)
+  {
+    sendChimeraCommand("listen stop select; " + command
+            + "; listen start select", false);
+  }
+
+  public void focus()
+  {
+    sendChimeraCommand("focus", false);
+  }
+
+  public void clearOnChimeraExit()
+  {
+    chimera = null;
+    currentModelsMap.clear();
+    chimeraListenerThreads = null;
+    structureManager.clearOnChimeraExit();
+  }
+
+  public void exitChimera()
+  {
+    if (isChimeraLaunched() && chimera != null)
+    {
+      sendChimeraCommand("stop really", false);
+      try
+      {
+        chimera.destroy();
+      } catch (Exception ex)
+      {
+        // ignore
+      }
+    }
+    clearOnChimeraExit();
+  }
+
+  public Map<Integer, ChimeraModel> getSelectedModels()
+  {
+    Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
+    List<String> chimeraReply = sendChimeraCommand(
+            "list selection level molecule", true);
+    if (chimeraReply != null)
+    {
+      for (String modelLine : chimeraReply)
+      {
+        ChimeraModel chimeraModel = new ChimeraModel(modelLine);
+        Integer modelKey = ChimUtils.makeModelKey(
+                chimeraModel.getModelNumber(),
+                chimeraModel.getSubModelNumber());
+        selectedModelsMap.put(modelKey, chimeraModel);
+      }
+    }
+    return selectedModelsMap;
+  }
+
+  public List<String> getSelectedResidueSpecs()
+  {
+    List<String> selectedResidues = new ArrayList<String>();
+    List<String> chimeraReply = sendChimeraCommand(
+            "list selection level residue", true);
+    if (chimeraReply != null)
+    {
+      for (String inputLine : chimeraReply)
+      {
+        String[] inputLineParts = inputLine.split("\\s+");
+        if (inputLineParts.length == 5)
+        {
+          selectedResidues.add(inputLineParts[2]);
+        }
+      }
+    }
+    return selectedResidues;
+  }
+
+  public void getSelectedResidues(
+          Map<Integer, ChimeraModel> selectedModelsMap)
+  {
+    List<String> chimeraReply = sendChimeraCommand(
+            "list selection level residue", true);
+    if (chimeraReply != null)
+    {
+      for (String inputLine : chimeraReply)
+      {
+        ChimeraResidue r = new ChimeraResidue(inputLine);
+        Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
+                r.getSubModelNumber());
+        if (selectedModelsMap.containsKey(modelKey))
+        {
+          ChimeraModel model = selectedModelsMap.get(modelKey);
+          model.addResidue(r);
+        }
+      }
+    }
+  }
+
+  /**
+   * Return the list of ChimeraModels currently open. Warning: if smiles model
+   * name too long, only part of it with "..." is printed.
+   * 
+   * 
+   * @return List of ChimeraModel's
+   */
+  // TODO: [Optional] Handle smiles names in a better way in Chimera?
+  public List<ChimeraModel> getModelList()
+  {
+    List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
+    List<String> list = sendChimeraCommand("list models type molecule",
+            true);
+    if (list != null)
+    {
+      for (String modelLine : list)
+      {
+        ChimeraModel chimeraModel = new ChimeraModel(modelLine);
+        modelList.add(chimeraModel);
+      }
+    }
+    return modelList;
+  }
+
+  /**
+   * Return the list of depiction presets available from within Chimera. Chimera
+   * will return the list as a series of lines with the format: Preset type
+   * number "description"
+   * 
+   * @return list of presets
+   */
+  public List<String> getPresets()
+  {
+    ArrayList<String> presetList = new ArrayList<String>();
+    List<String> output = sendChimeraCommand("preset list", true);
+    if (output != null)
+    {
+      for (String preset : output)
+      {
+        preset = preset.substring(7); // Skip over the "Preset"
+        preset = preset.replaceFirst("\"", "(");
+        preset = preset.replaceFirst("\"", ")");
+        // string now looks like: type number (description)
+        presetList.add(preset);
+      }
+    }
+    return presetList;
+  }
+
+  public boolean isChimeraLaunched()
+  {
+    // TODO: [Optional] What is the best way to test if chimera is launched?
+
+    // sendChimeraCommand("test", true) !=null
+    if (chimera != null)
+    {
+      return true;
+    }
+    return false;
+  }
+
+  public boolean launchChimera(List<String> chimeraPaths)
+  {
+    // Do nothing if Chimera is already launched
+    if (isChimeraLaunched())
+    {
+      return true;
+    }
+
+    // Try to launch Chimera (eventually using one of the possible paths)
+    String error = "Error message: ";
+    String workingPath = "";
+    // iterate over possible paths for starting Chimera
+    for (String chimeraPath : chimeraPaths)
+    {
+      File path = new File(chimeraPath);
+      if (!path.canExecute())
+      {
+        error += "File '" + path + "' does not exist.\n";
+        continue;
+      }
+      try
+      {
+        List<String> args = new ArrayList<String>();
+        args.add(chimeraPath);
+        args.add("--start");
+        args.add("ReadStdin");
+        ProcessBuilder pb = new ProcessBuilder(args);
+        chimera = pb.start();
+        error = "";
+        workingPath = chimeraPath;
+        logger.info("Strarting " + chimeraPath);
+        break;
+      } catch (Exception e)
+      {
+        // Chimera could not be started
+        error += e.getMessage();
+      }
+    }
+    // If no error, then Chimera was launched successfully
+    if (error.length() == 0)
+    {
+      // Initialize the listener threads
+      chimeraListenerThreads = new ListenerThreads(chimera,
+              structureManager);
+      chimeraListenerThreads.start();
+      // structureManager.initChimTable();
+      structureManager.setChimeraPathProperty(workingPath);
+      // TODO: [Optional] Check Chimera version and show a warning if below 1.8
+      // Ask Chimera to give us updates
+      startListening();
+      return true;
+    }
+
+    // Tell the user that Chimera could not be started because of an error
+    logger.warn(error);
+    return false;
+  }
+
+  /**
+   * Determine the color that Chimera is using for this model.
+   * 
+   * @param model
+   *          the ChimeraModel we want to get the Color for
+   * @return the default model Color for this model in Chimera
+   */
+  public Color getModelColor(ChimeraModel model)
+  {
+    List<String> colorLines = sendChimeraCommand(
+            "list model spec " + model.toSpec() + " attribute color", true);
+    if (colorLines == null || colorLines.size() == 0)
+    {
+      return null;
+    }
+    return ChimUtils.parseModelColor((String) colorLines.get(0));
+  }
+
+  /**
+   * 
+   * Get information about the residues associated with a model. This uses the
+   * Chimera listr command. We don't return the resulting residues, but we add
+   * the residues to the model.
+   * 
+   * @param model
+   *          the ChimeraModel to get residue information for
+   * 
+   */
+  public void addResidues(ChimeraModel model)
+  {
+    int modelNumber = model.getModelNumber();
+    int subModelNumber = model.getSubModelNumber();
+    // Get the list -- it will be in the reply log
+    List<String> reply = sendChimeraCommand(
+            "list residues spec " + model.toSpec(), true);
+    if (reply == null)
+    {
+      return;
+    }
+    for (String inputLine : reply)
+    {
+      ChimeraResidue r = new ChimeraResidue(inputLine);
+      if (r.getModelNumber() == modelNumber
+              || r.getSubModelNumber() == subModelNumber)
+      {
+        model.addResidue(r);
+      }
+    }
+  }
+
+  public List<String> getAttrList()
+  {
+    List<String> attributes = new ArrayList<String>();
+    final List<String> reply = sendChimeraCommand("list resattr", true);
+    if (reply != null)
+    {
+      for (String inputLine : reply)
+      {
+        String[] lineParts = inputLine.split("\\s");
+        if (lineParts.length == 2 && lineParts[0].equals("resattr"))
+        {
+          attributes.add(lineParts[1]);
+        }
+      }
+    }
+    return attributes;
+  }
+
+  public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
+          ChimeraModel model)
+  {
+    Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
+    final List<String> reply = sendChimeraCommand("list residue spec "
+            + model.toSpec() + " attribute " + aCommand, true);
+    if (reply != null)
+    {
+      for (String inputLine : reply)
+      {
+        String[] lineParts = inputLine.split("\\s");
+        if (lineParts.length == 5)
+        {
+          ChimeraResidue residue = ChimUtils
+                  .getResidue(lineParts[2], model);
+          String value = lineParts[4];
+          if (residue != null)
+          {
+            if (value.equals("None"))
+            {
+              continue;
+            }
+            if (value.equals("True") || value.equals("False"))
+            {
+              values.put(residue, Boolean.valueOf(value));
+              continue;
+            }
+            try
+            {
+              Double doubleValue = Double.valueOf(value);
+              values.put(residue, doubleValue);
+            } catch (NumberFormatException ex)
+            {
+              values.put(residue, value);
+            }
+          }
+        }
+      }
+    }
+    return values;
+  }
+
+  /**
+   * Send a command to Chimera.
+   * 
+   * @param command
+   *          Command string to be send.
+   * @param reply
+   *          Flag indicating whether the method should return the reply from
+   *          Chimera or not.
+   * @return List of Strings corresponding to the lines in the Chimera reply or
+   *         <code>null</code>.
+   */
+  public List<String> sendChimeraCommand(String command, boolean reply)
+  {
+    if (!isChimeraLaunched())
+    {
+      return null;
+    }
+
+    chimeraListenerThreads.clearResponse(command);
+    String text = command.concat("\n");
+    // System.out.println("send command to chimera: " + text);
+    try
+    {
+      // send the command
+      chimera.getOutputStream().write(text.getBytes());
+      chimera.getOutputStream().flush();
+    } catch (IOException e)
+    {
+      // logger.info("Unable to execute command: " + text);
+      // logger.info("Exiting...");
+      logger.warn("Unable to execute command: " + text);
+      logger.warn("Exiting...");
+      clearOnChimeraExit();
+      return null;
+    }
+    if (!reply)
+    {
+      return null;
+    }
+    return chimeraListenerThreads.getResponse(command);
+  }
+
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraModel.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraModel.java
new file mode 100644 (file)
index 0000000..432c6fb
--- /dev/null
@@ -0,0 +1,466 @@
+package ext.edu.ucsf.rbvi.strucviz2;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
+
+/**
+ * This class provides the implementation for the ChimeraModel, ChimeraChain, and ChimeraResidue
+ * objects
+ * 
+ * @author scooter
+ * 
+ */
+public class ChimeraModel implements ChimeraStructuralObject {
+
+       private String name; // The name of this model
+       private ModelType type; // The type of the model
+       private int modelNumber; // The model number
+       private int subModelNumber; // The sub-model number
+
+       private Color modelColor = null; // The color of this model (from Chimera)
+       private Object userData = null; // User data associated with this model
+       private boolean selected = false; // The selected state of this model
+
+       private TreeMap<String, ChimeraChain> chainMap; // The list of chains
+       // private TreeMap<String, ChimeraResidue> residueMap; // The list of residues
+       private HashSet<ChimeraResidue> funcResidues; // List of functional residues
+
+       /**
+        * Constructor to create a model
+        * 
+        * @param name
+        *            the name of this model
+        * @param color
+        *            the model Color
+        * @param modelNumber
+        *            the model number
+        * @param subModelNumber
+        *            the sub-model number
+        */
+       public ChimeraModel(String name, ModelType type, int modelNumber, int subModelNumber) {
+               this.name = name;
+               this.type = type;
+               this.modelNumber = modelNumber;
+               this.subModelNumber = subModelNumber;
+
+               this.chainMap = new TreeMap<String, ChimeraChain>();
+               this.funcResidues = new HashSet<ChimeraResidue>();
+       }
+
+       /**
+        * Constructor to create a model from the Chimera input line
+        * 
+        * @param inputLine
+        *            Chimera input line from which to construct this model
+        */
+       // TODO: [Optional] How to distinguish between PDB and MODBASE?
+       // invoked when listing models: listm type molecule; lists level molecule
+       // line = model id #0 type Molecule name 1ert
+       public ChimeraModel(String inputLine) {
+               this.name = ChimUtils.parseModelName(inputLine);
+               // TODO: [Optional] Write a separate method to get model type
+               if (name.startsWith("smiles")) {
+                       this.type = ModelType.SMILES;
+               } else {
+                       this.type = ModelType.PDB_MODEL;
+               }
+               this.modelNumber = ChimUtils.parseModelNumber(inputLine)[0];
+               this.subModelNumber = ChimUtils.parseModelNumber(inputLine)[1];
+
+               this.chainMap = new TreeMap<String, ChimeraChain>();
+               this.funcResidues = new HashSet<ChimeraResidue>();
+       }
+
+       /**
+        * Add a residue to this model
+        * 
+        * @param residue
+        *            to add to the model
+        */
+       public void addResidue(ChimeraResidue residue) {
+               residue.setChimeraModel(this);
+               // residueMap.put(residue.getIndex(), residue);
+               String chainId = residue.getChainId();
+               if (chainId != null) {
+                       addResidue(chainId, residue);
+               } else {
+                       addResidue("_", residue);
+               }
+               // Put it in our map so that we can return it in order
+               // residueMap.put(residue.getIndex(), residue);
+       }
+
+       /**
+        * Add a residue to a chain in this model. If the chain associated with chainId doesn't exist,
+        * it will be created.
+        * 
+        * @param chainId
+        *            to add the residue to
+        * @param residue
+        *            to add to the chain
+        */
+       public void addResidue(String chainId, ChimeraResidue residue) {
+               ChimeraChain chain = null;
+               if (!chainMap.containsKey(chainId)) {
+                       chain = new ChimeraChain(this.modelNumber, this.subModelNumber, chainId);
+                       chain.setChimeraModel(this);
+                       chainMap.put(chainId, chain);
+               } else {
+                       chain = chainMap.get(chainId);
+               }
+               chain.addResidue(residue);
+       }
+
+       /**
+        * Get the ChimeraModel (required for ChimeraStructuralObject interface)
+        * 
+        * @return ChimeraModel
+        */
+       public ChimeraModel getChimeraModel() {
+               return this;
+       }
+
+       /**
+        * Get the model color of this model
+        * 
+        * @return model color of this model
+        */
+       public Color getModelColor() {
+               return this.modelColor;
+       }
+
+       /**
+        * Set the color of this model
+        * 
+        * @param color
+        *            Color of this model
+        */
+       public void setModelColor(Color color) {
+               this.modelColor = color;
+       }
+
+       /**
+        * Return the name of this model
+        * 
+        * @return model name
+        */
+       public String getModelName() {
+               return name;
+       }
+
+       /**
+        * Set the name of this model
+        * 
+        * @param name
+        *            model name
+        */
+       public void setModelName(String name) {
+               this.name = name;
+       }
+
+       /**
+        * Get the model number of this model
+        * 
+        * @return integer model number
+        */
+       public int getModelNumber() {
+               return modelNumber;
+       }
+
+       /**
+        * Set the model number of this model
+        * 
+        * @param modelNumber
+        *            integer model number
+        */
+       public void setModelNumber(int modelNumber) {
+               this.modelNumber = modelNumber;
+       }
+
+       /**
+        * Get the sub-model number of this model
+        * 
+        * @return integer sub-model number
+        */
+       public int getSubModelNumber() {
+               return subModelNumber;
+       }
+
+       /**
+        * Set the sub-model number of this model
+        * 
+        * @param subModelNumber
+        *            integer model number
+        */
+       public void setSubModelNumber(int subModelNumber) {
+               this.subModelNumber = subModelNumber;
+       }
+
+       public ModelType getModelType() {
+               return type;
+       }
+
+       public void setModelType(ModelType type) {
+               this.type = type;
+       }
+
+       public HashSet<ChimeraResidue> getFuncResidues() {
+               return funcResidues;
+       }
+
+       public void setFuncResidues(List<String> residues) {
+               for (String residue : residues) {
+                       for (ChimeraChain chain : getChains()) {
+                               if (residue.indexOf("-") > 0) {
+                                       funcResidues.addAll(chain.getResidueRange(residue));
+                               } else {
+                                       funcResidues.add(chain.getResidue(residue));
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Get the user data for this model
+        * 
+        * @return user data
+        */
+       public Object getUserData() {
+               return userData;
+       }
+
+       /**
+        * Set the user data for this model
+        * 
+        * @param data
+        *            user data to associate with this model
+        */
+       public void setUserData(Object data) {
+               this.userData = data;
+       }
+
+       /**
+        * Return the selected state of this model
+        * 
+        * @return the selected state
+        */
+       public boolean isSelected() {
+               return selected;
+       }
+
+       /**
+        * Set the selected state of this model
+        * 
+        * @param selected
+        *            a boolean to set the selected state to
+        */
+       public void setSelected(boolean selected) {
+               this.selected = selected;
+       }
+
+       /**
+        * Return the chains in this model as a List
+        * 
+        * @return the chains in this model as a list
+        */
+       public List<ChimeraStructuralObject> getChildren() {
+               return new ArrayList<ChimeraStructuralObject>(chainMap.values());
+       }
+
+       /**
+        * Return the chains in this model as a colleciton
+        * 
+        * @return the chains in this model
+        */
+       public Collection<ChimeraChain> getChains() {
+               return chainMap.values();
+       }
+
+       /**
+        * Get the number of chains in this model
+        * 
+        * @return integer chain count
+        */
+       public int getChainCount() {
+               return chainMap.size();
+       }
+
+       /**
+        * Get the list of chain names associated with this model
+        * 
+        * @return return the list of chain names for this model
+        */
+       public Collection<String> getChainNames() {
+               return chainMap.keySet();
+       }
+
+       /**
+        * Get the residues associated with this model
+        * 
+        * @return the list of residues in this model
+        */
+       public Collection<ChimeraResidue> getResidues() {
+               Collection<ChimeraResidue> residues = new ArrayList<ChimeraResidue>();
+               for (ChimeraChain chain : getChains()) {
+                       residues.addAll(chain.getResidues());
+               }
+               return residues;
+       }
+
+       /**
+        * Get the number of residues in this model
+        * 
+        * @return integer residues count
+        */
+       public int getResidueCount() {
+               int count = 0;
+               for (ChimeraChain chain : getChains()) {
+                       count += chain.getResidueCount();
+               }
+               return count;
+       }
+
+       /**
+        * Get a specific chain from the model
+        * 
+        * @param chain
+        *            the ID of the chain to return
+        * @return ChimeraChain associated with the chain
+        */
+       public ChimeraChain getChain(String chain) {
+               if (chainMap.containsKey(chain)) {
+                       return chainMap.get(chain);
+               }
+               return null;
+       }
+
+       /**
+        * Return a specific residue based on its index
+        * 
+        * @param index
+        *            of the residue to return
+        * @return the residue associated with that index
+        */
+       public ChimeraResidue getResidue(String chainId, String index) {
+               if (chainMap.containsKey(chainId)) {
+                       return chainMap.get(chainId).getResidue(index);
+               }
+               return null;
+       }
+
+       /**
+        * Checks if this model has selected children.
+        */
+       public boolean hasSelectedChildren() {
+               if (selected) {
+                       return true;
+               } else {
+                       for (ChimeraChain chain : getChains()) {
+                               if (chain.hasSelectedChildren()) {
+                                       return true;
+                               }
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Return the list of selected residues
+        * 
+        * @return all selected residues
+        */
+       public List<ChimeraResidue> getSelectedResidues() {
+               List<ChimeraResidue> residueList = new ArrayList<ChimeraResidue>();
+               for (ChimeraChain chain : getChains()) {
+                       if (selected) {
+                               residueList.addAll(chain.getSelectedResidues());
+                       } else {
+                               residueList.addAll(getResidues());
+                       }
+               }
+               return residueList;
+       }
+
+
+       /**
+        * Return the Chimera specification for this model.
+        */
+       public String toSpec() {
+               if (subModelNumber == 0)
+                       return ("#" + modelNumber);
+               return ("#" + modelNumber + "." + subModelNumber);
+       }
+
+       /**
+        * Return a string representation for the model. Shorten if longer than 100 characters.
+        */
+       public String toString() {
+               String modelName = "";
+               // TODO: [Optional] Change cutoff for shortening model names in the structure naviagator dialog
+               if (getChainCount() > 0) {
+                       modelName = "Model " + toSpec() + " " + name + " (" + getChainCount() + " chains, "
+                                       + getResidueCount() + " residues)";
+               } else if (getResidueCount() > 0) {
+                       modelName = "Model " + toSpec() + " " + name + " (" + getResidueCount() + " residues)";
+               } else {
+                       modelName = "Model " + toSpec() + " " + name + "";
+               }
+
+               Set<String> networkNames = new HashSet<String>();
+               Set<String> nodeNames = new HashSet<String>();
+               Set<String> edgeNames = new HashSet<String>();
+
+               String cytoName = " [";
+               if (networkNames.size() > 0) {
+                       if (networkNames.size() == 1) {
+                               cytoName += "Network {";
+                       } else if (networkNames.size() > 1) {
+                               cytoName += "Networks {";
+                       }
+                       for (String cName : networkNames) {
+                               cytoName += cName + ",";
+                       }
+                       cytoName = cytoName.substring(0, cytoName.length() - 1) + "}, ";
+               }
+               if (nodeNames.size() > 0) {
+                       if (nodeNames.size() == 1) {
+                               cytoName += "Node {";
+                       } else if (nodeNames.size() > 1) {
+                               cytoName += "Nodes {";
+                       }
+                       for (String cName : nodeNames) {
+                               cytoName += cName + ",";
+                       }
+                       cytoName = cytoName.substring(0, cytoName.length() - 1) + "}, ";
+               }
+               if (edgeNames.size() > 0) {
+                       if (edgeNames.size() == 1) {
+                               cytoName += "Edge {";
+                       } else if (edgeNames.size() > 1) {
+                               cytoName += "Edges {";
+                       }
+                       for (String cName : edgeNames) {
+                               cytoName += cName + ",";
+                       }
+                       cytoName = cytoName.substring(0, cytoName.length() - 1) + "}, ";
+               }
+               if (cytoName.endsWith(", ")) {
+                       cytoName = cytoName.substring(0, cytoName.length() - 2);
+               }
+               cytoName += "]";
+               String nodeName = modelName + cytoName;
+               if (nodeName.length() > 100) {
+                       nodeName = nodeName.substring(0, 100) + "...";
+               }
+               return nodeName;
+       }
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraResidue.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraResidue.java
new file mode 100644 (file)
index 0000000..10825ae
--- /dev/null
@@ -0,0 +1,394 @@
+/* vim: set ts=2: */
+/**
+ * Copyright (c) 2006 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions, and the following disclaimer.
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions, and the following
+ *      disclaimer in the documentation and/or other materials provided
+ *      with the distribution.
+ *   3. Redistributions must acknowledge that this software was
+ *      originally developed by the UCSF Computer Graphics Laboratory
+ *      under support by the NIH National Center for Research Resources,
+ *      grant P41-RR01081.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package ext.edu.ucsf.rbvi.strucviz2;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class provides the implementation for the ChimeraResidue, object
+ * 
+ * @author scooter
+ * 
+ */
+
+public class ChimeraResidue implements ChimeraStructuralObject, Comparable<ChimeraResidue> {
+
+       /* Constants */
+       public static final int SINGLE_LETTER = 0; // Display residues as a single
+                                                                                                                                                                                       // letter
+       public static final int THREE_LETTER = 1; // Display residues as three letters
+       public static final int FULL_NAME = 2; // Display full residue names
+
+       private String type; // Residue type
+       private String index; // Residue index
+       private String chainId; // ChainID for this residue
+       private int modelNumber; // model number for this residue
+       private int subModelNumber; // sub-model number for this residue
+       protected int residueNumber;
+       protected String insertionCode;
+       private ChimeraModel chimeraModel; // ChimeraModel the residue is part of
+       private Object userData; // user data to associate with this residue
+       // public static HashMap<String, String> aaNames = null; // a map of amino acid
+       // names
+       private static int displayType = THREE_LETTER; // the current display type
+       private boolean selected = false; // the selection state
+
+       /**
+        * Constructor to create a new ChimeraResidue
+        * 
+        * @param type
+        *          the residue type
+        * @param index
+        *          the index of the residue
+        * @param modelNumber
+        *          the model number this residue is part of
+        */
+       public ChimeraResidue(String type, String index, int modelNumber) {
+               this(type, index, modelNumber, 0);
+       }
+
+       /**
+        * Constructor to create a new ChimeraResidue
+        * 
+        * @param type
+        *          the residue type
+        * @param index
+        *          the index of the residue
+        * @param modelNumber
+        *          the model number this residue is part of
+        * @param subModelNumber
+        *          the sub-model number this residue is part of
+        */
+       public ChimeraResidue(String type, String index, int modelNumber, int subModelNumber) {
+               this.type = type;
+               this.index = index;
+               this.modelNumber = modelNumber;
+               this.subModelNumber = subModelNumber;
+               splitInsertionCode(this.index);
+               // if (aaNames == null) {
+               // initNames();
+               // }
+       }
+
+       /**
+        * Constructor to create a new ChimeraResidue from an input line
+        * 
+        * @param chimeraInputLine
+        *          a Chimera residue description
+        */
+       // invoked when listing (selected) residues: listr spec #0; lists level residue
+       // Line: residue id #0:37.A type MET
+       public ChimeraResidue(String chimeraInputLine) {
+               // initNames();
+               String[] split1 = chimeraInputLine.split(":");
+
+               // First half has model number -- get the number
+               int numberOffset = split1[0].indexOf('#');
+               String model = split1[0].substring(numberOffset + 1);
+               int decimalOffset = model.indexOf('.'); // Do we have a sub-model?
+               try {
+                       this.subModelNumber = 0;
+                       if (decimalOffset > 0) {
+                               this.subModelNumber = Integer.parseInt(model.substring(decimalOffset + 1));
+                               this.modelNumber = Integer.parseInt(model.substring(0, decimalOffset));
+                       } else {
+                               this.modelNumber = Integer.parseInt(model);
+                       }
+               } catch (Exception e) {
+                       LoggerFactory.getLogger(ChimeraResidue.class)
+                                       .error("Unexpected return from Chimera: " + model);
+                       this.modelNumber = -1;
+               }
+
+               // Second half has residue info: index & type
+               String[] rTokens = split1[1].split(" ");
+               this.type = rTokens[2];
+
+               String[] iTokens = rTokens[0].split("\\.");
+               if (iTokens.length > 0) {
+                       this.index = iTokens[0];
+
+                       // Careful, might or might not have a chainID
+                       if (iTokens.length > 1)
+                               this.chainId = iTokens[1];
+                       else
+                               this.chainId = "_";
+               } else
+                       this.index = rTokens[0];
+
+               splitInsertionCode(this.index);
+       }
+
+       /**
+        * Set the selected state for this residue
+        * 
+        * @param selected
+        *          the selection state to set
+        */
+       public void setSelected(boolean selected) {
+               this.selected = selected;
+       }
+
+       /**
+        * Return the selected state of this residue
+        * 
+        * @return the selected state
+        */
+       public boolean isSelected() {
+               return selected;
+       }
+
+       /**
+        * Return an array made up of this residue (required for ChimeraStructuralObject interface
+        * 
+        * @return a List with this residue as its sole member
+        */
+       public List<ChimeraStructuralObject> getChildren() {
+               List<ChimeraStructuralObject> v = new ArrayList<ChimeraStructuralObject>();
+               v.add(this);
+               return v;
+       }
+
+       /**
+        * Return the string representation of this residue as follows: "<i>residue_name</i> <i>index</i>"
+        * where <i>residue_name</i> could be either the single letter, three letter, or full name
+        * representation of the amino acid.
+        * 
+        * @return the string representation
+        */
+       public String displayName() {
+               return toString();
+       }
+
+       /**
+        * Return the string representation of this residue as follows: "<i>residue_name</i> <i>index</i>"
+        * where <i>residue_name</i> could be either the single letter, three letter, or full name
+        * representation of the amino acid.
+        * 
+        * @return the string representation
+        */
+       public String toString() {
+               if (displayType == FULL_NAME) {
+                       return (ChimUtils.toFullName(type) + " " + index);
+               } else if (displayType == SINGLE_LETTER) {
+                       return (ChimUtils.toSingleLetter(type) + " " + index);
+               } else if (displayType == THREE_LETTER) {
+                       return (ChimUtils.toThreeLetter(type) + " " + index);
+               } else {
+                       return (type + " " + index);
+               }
+       }
+
+       /**
+        * Return the Chimera specification for this Residue
+        * 
+        * @return Chimera specification
+        */
+       public String toSpec() {
+               if (!chainId.equals("_"))
+                       return ("#" + modelNumber + ":" + index + "." + chainId);
+               else
+                       return ("#" + modelNumber + ":" + index + ".");
+       }
+
+       /**
+        * Get the index of this residue
+        * 
+        * @return residue index
+        */
+       public String getIndex() {
+               return this.index;
+       }
+
+       /**
+        * Get the chainID for this residue
+        * 
+        * @return String value of the chainId
+        */
+       public String getChainId() {
+               return this.chainId;
+       }
+
+       /**
+        * Get the type for this residue
+        * 
+        * @return residue type
+        */
+       public String getType() {
+               return this.type;
+       }
+
+       /**
+        * Get the model number for this residue
+        * 
+        * @return the model number
+        */
+       public int getModelNumber() {
+               return this.modelNumber;
+       }
+
+       /**
+        * Get the sub-model number for this residue
+        * 
+        * @return the sub-model number
+        */
+       public int getSubModelNumber() {
+               return this.subModelNumber;
+       }
+
+       /**
+        * Get the model this residue is part of
+        * 
+        * @return the ChimeraModel
+        */
+       public ChimeraModel getChimeraModel() {
+               return this.chimeraModel;
+       }
+
+       /**
+        * Set the model this residue is part of
+        * 
+        * @param chimeraModel
+        *          the ChimeraModel this model is part of
+        */
+       public void setChimeraModel(ChimeraModel chimeraModel) {
+               this.chimeraModel = chimeraModel;
+       }
+
+       /**
+        * Get the user data for this residue
+        * 
+        * @return user data
+        */
+       public Object getUserData() {
+               return userData;
+       }
+
+       /**
+        * Set the user data for this Residue
+        * 
+        * @param data
+        *          the user data to associate with this residue
+        */
+       public void setUserData(Object data) {
+               this.userData = data;
+       }
+
+       public int compareTo(ChimeraResidue c2) {
+               if (residueNumber < c2.residueNumber)
+                       return -1;
+               else if (residueNumber == c2.residueNumber) {
+                       if (insertionCode == null && c2.insertionCode == null)
+                               return 0;
+                       else if (insertionCode == null)
+                               return -1;
+                       else if (c2.insertionCode == null)
+                               return 1;
+                       return (insertionCode.compareTo(c2.insertionCode));
+               }
+               return 1;
+       }
+
+       public void splitInsertionCode(String residue) {
+               // OK, split the index into number and insertion code
+               Pattern p = Pattern.compile("(\\d*)([A-Z]?)");
+               Matcher m = p.matcher(residue);
+               if (m.matches()) {
+                       this.residueNumber = Integer.parseInt(m.group(1));
+                       if (m.groupCount() > 1)
+                               this.insertionCode = m.group(2);
+                       else
+                               this.insertionCode = null;
+               }
+       }
+
+       /**********************************************
+        * Static routines
+        *********************************************/
+
+       /**
+        * Initialize the residue names
+        */
+       // private static void initNames() {
+       // // Create our residue name table
+       // aaNames = new HashMap<String, String>();
+       // aaNames.put("ALA", "A Ala Alanine N[C@@H](C)C(O)=O");
+       // aaNames.put("ARG", "R Arg Arginine N[C@@H](CCCNC(N)=N)C(O)=O");
+       // aaNames.put("ASN", "N Asn Asparagine N[C@@H](CC(N)=O)C(O)=O");
+       // aaNames.put("ASP", "D Asp Aspartic_acid N[C@@H](CC(O)=O)C(O)=O");
+       // aaNames.put("CYS", "C Cys Cysteine N[C@@H](CS)C(O)=O");
+       // aaNames.put("GLN", "Q Gln Glutamine N[C@H](C(O)=O)CCC(N)=O");
+       // aaNames.put("GLU", "E Glu Glumatic_acid N[C@H](C(O)=O)CCC(O)=O");
+       // aaNames.put("GLY", "G Gly Glycine NCC(O)=O");
+       // aaNames.put("HIS", "H His Histidine N[C@@H](CC1=CN=CN1)C(O)=O");
+       // aaNames.put("ILE", "I Ile Isoleucine N[C@]([C@H](C)CC)([H])C(O)=O");
+       // aaNames.put("LEU", "L Leu Leucine N[C@](CC(C)C)([H])C(O)=O");
+       // aaNames.put("LYS", "K Lys Lysine N[C@](CCCCN)([H])C(O)=O");
+       // aaNames.put("DLY", "K Dly D-Lysine NCCCC[C@@H](N)C(O)=O");
+       // aaNames.put("MET", "M Met Methionine N[C@](CCSC)([H])C(O)=O");
+       // aaNames.put("PHE", "F Phe Phenylalanine N[C@](CC1=CC=CC=C1)([H])C(O)=O");
+       // aaNames.put("PRO", "P Pro Proline OC([C@@]1([H])NCCC1)=O");
+       // aaNames.put("SER", "S Ser Serine OC[C@](C(O)=O)([H])N");
+       // aaNames.put("THR", "T Thr Threonine O[C@H](C)[C@](C(O)=O)([H])N");
+       // aaNames.put("TRP", "W Trp Tryptophan N[C@@]([H])(CC1=CN([H])C2=C1C=CC=C2)C(O)=O");
+       // aaNames.put("TYR", "Y Tyr Tyrosine N[C@@](C(O)=O)([H])CC1=CC=C(O)C=C1");
+       // aaNames.put("VAL", "V Val Valine N[C@@](C(O)=O)([H])C(C)C");
+       // aaNames.put("ASX", "B Asx Aspartic_acid_or_Asparagine");
+       // aaNames.put("GLX", "Z Glx Glutamine_or_Glutamic_acid");
+       // aaNames.put("XAA", "X Xaa Any_or_unknown_amino_acid");
+       // aaNames.put("HOH", "HOH HOH Water [H]O[H]");
+       // }
+
+       /**
+        * Set the display type.
+        * 
+        * @param type
+        *          the display type
+        */
+       public static void setDisplayType(int type) {
+               displayType = type;
+       }
+
+       public static int getDisplayType() {
+               return displayType;
+       }
+
+       public boolean hasSelectedChildren() {
+               return false;
+       }
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraStructuralObject.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraStructuralObject.java
new file mode 100644 (file)
index 0000000..1678317
--- /dev/null
@@ -0,0 +1,112 @@
+/* vim: set ts=2: */
+/**
+ * Copyright (c) 2006 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions, and the following disclaimer.
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions, and the following
+ *      disclaimer in the documentation and/or other materials provided
+ *      with the distribution.
+ *   3. Redistributions must acknowledge that this software was
+ *      originally developed by the UCSF Computer Graphics Laboratory
+ *      under support by the NIH National Center for Research Resources,
+ *      grant P41-RR01081.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package ext.edu.ucsf.rbvi.strucviz2;
+
+import java.lang.String;
+import java.util.List;
+
+/**
+ * This interface provides a common set of methods that are implemented by the ChimeraModel,
+ * ChimeraChain, and ChimeraResidue classes.
+ * 
+ * @author scooter
+ * 
+ */
+
+public interface ChimeraStructuralObject {
+
+       /**
+        * Return the Chimera selection specification for this object
+        * 
+        * @return a String representing a Chimera atom-spec
+        */
+       public String toSpec();
+
+       /**
+        * Return a String representation for this object
+        * 
+        * @return a String representing the object name
+        */
+       public String toString();
+
+       /**
+        * Return the userData for this object
+        * 
+        * @return an Object representing the userData (usually TreePath)
+        */
+       public Object getUserData();
+
+       /**
+        * Set the userData for this object
+        * 
+        * @param userData
+        *          the Object representing the userData (usually TreePath)
+        */
+       public void setUserData(Object userData);
+
+       /**
+        * Return the ChimeraModel for this object
+        * 
+        * @return the ChimeraModel this object is part of
+        */
+       public ChimeraModel getChimeraModel();
+
+       /**
+        * Set the "selected" state of this object
+        * 
+        * @param selected
+        *          boolean value as to whether this object is selected
+        */
+       public void setSelected(boolean selected);
+
+       /**
+        * Get the "selected" state of this object
+        * 
+        * @return the selected state of this object
+        */
+       public boolean isSelected();
+
+       /**
+        * Get the selected state of this object and its children.
+        * 
+        * @return true if any child is selected.
+        */
+       public boolean hasSelectedChildren();
+
+       /**
+        * Get the children of this object (if any)
+        * 
+        * @return the children of the object
+        */
+       public List<ChimeraStructuralObject> getChildren();
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraTreeModel.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraTreeModel.java
new file mode 100644 (file)
index 0000000..f3447ce
--- /dev/null
@@ -0,0 +1,194 @@
+/* vim: set ts=2: */
+/**
+ * Copyright (c) 2006 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions, and the following disclaimer.
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions, and the following
+ *      disclaimer in the documentation and/or other materials provided
+ *      with the distribution.
+ *   3. Redistributions must acknowledge that this software was
+ *      originally developed by the UCSF Computer Graphics Laboratory
+ *      under support by the NIH National Center for Research Resources,
+ *      grant P41-RR01081.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package ext.edu.ucsf.rbvi.strucviz2;
+
+// System imports
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.JTree;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+// Cytoscape imports
+// StructureViz imports
+
+/**
+ * The ChimeraTreeModel class provides the underlying model
+ * for the navigation tree in the ModelNavigatorDialog.
+ *
+ * @author scooter
+ * @see ModelNavigatorDialog
+        */
+public class ChimeraTreeModel extends DefaultTreeModel {
+       private ChimeraManager chimeraManager;
+       private JTree navigationTree;
+       private int residueDisplay = ChimeraResidue.THREE_LETTER;
+
+       /**
+        * Constructor for the ChimeraTreeModel.
+        *
+        * @param chimeraObject the Chimera object that this tree represents
+        * @param tree the JTree used to display the object
+        * @see Chimera
+        */
+       public ChimeraTreeModel (ChimeraManager chimeraManager, JTree tree) {
+               super(new DefaultMutableTreeNode());
+               this.chimeraManager = chimeraManager;
+               this.navigationTree = tree;
+               DefaultMutableTreeNode rootNode = buildTree();
+               this.setRoot(rootNode);
+       }
+
+       /**
+        * Set the display type for the residues.  The display type
+        * must be one of:
+        *
+        *      ChimeraResidue.THREE_LETTER
+        *      ChimeraResidue.SINGLE_LETTER
+        *      ChimeraResidue.FULL_NAME
+        *
+        * @param newDisplay the display type
+        * @see ChimeraResidue
+        */
+       public void setResidueDisplay(int newDisplay) {
+               this.residueDisplay = newDisplay;
+       }
+               
+       /**
+        * This method is called to rebuild the tree model "from scratch"
+        */
+       public void reload() {
+               // First, rebuild the tree with the new data
+               DefaultMutableTreeNode rootNode = buildTree();
+               this.setRoot(rootNode);
+
+               // Now let the superclass do all of the work
+               super.reload();
+       }
+
+       /**
+        * Rebuild an existing tree
+        */
+       public void rebuildTree() {
+               DefaultMutableTreeNode rootNode = buildTree();
+               DefaultTreeModel model = (DefaultTreeModel)navigationTree.getModel();
+               model.setRoot(rootNode);
+               model.reload();
+       }
+
+       /**
+        * Build the tree from the current Chimera data
+        *
+        * @return DefaultMutableTreeNode that represents the currently loaded Chimera models
+        */
+       private DefaultMutableTreeNode buildTree() {
+               int modelCount = chimeraManager.getChimeraModelsCount(true);
+               DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(modelCount+" Open Chimera Models");
+               TreePath rootPath = new TreePath(rootNode);
+
+               TreePath path = null;
+               DefaultMutableTreeNode model = null;
+
+               // Add all of the Chimera models
+               for (ChimeraModel chimeraModel: chimeraManager.getChimeraModels()) {
+                       model = new DefaultMutableTreeNode(chimeraModel);
+                       path = rootPath.pathByAddingChild(model);
+                       chimeraModel.setUserData(path);
+                       addChainNodes(chimeraModel, model, path);
+                       rootNode.add(model);
+               }
+               return rootNode;
+       }
+
+       /**
+        * add chains to a tree model
+        *
+        * @param chimeraModel the ChimeraModel to get the chains from
+        * @param treeModel the tree model to add the chains to
+        * @param treePath the tree path where the chains should be added
+        */
+       private void addChainNodes(ChimeraModel chimeraModel, 
+                                                                                                                DefaultMutableTreeNode treeModel,
+                                                                                                                TreePath treePath) {
+               DefaultMutableTreeNode chain = null;
+               TreePath chainPath = null;
+
+               // Get the list of chains
+               Collection<ChimeraChain> chainList = chimeraModel.getChains();
+
+               if (chainList.size() == 0) {
+                       // No chains!  Just add the residues
+                       addResidues(chimeraModel.getResidues(), treeModel, treePath);   
+                       return;
+               }
+
+               // Iterate over the chains and add the chain and all of
+               // the chain's residues
+               for (ChimeraChain chimeraChain: chainList) {
+                       chain = new DefaultMutableTreeNode(chimeraChain);
+                       chainPath = treePath.pathByAddingChild(chain);
+                       chimeraChain.setUserData(chainPath);
+                       addResidues(chimeraChain.getResidues(), chain, chainPath);
+                       treeModel.add(chain);   
+               }
+       }
+
+       /**
+        * add residues to a tree model
+        *
+        * @param residues the residues to add
+        * @param treeModel the tree model to add the residues to
+        * @param treePath the tree path where the residues should be added
+        */
+       private void addResidues(Collection<ChimeraResidue> residues, 
+                                                                                                        DefaultMutableTreeNode treeModel,
+                                                                                                        TreePath treePath) {
+               DefaultMutableTreeNode residue = null;
+               TreePath residuePath = null;
+
+               List<ChimeraResidue> sortedResidues = new ArrayList<ChimeraResidue>(residues);
+               Collections.sort(sortedResidues);
+
+               // Iterate over all residues & add them to the tree
+               for (ChimeraResidue res: sortedResidues) {
+                       res.setDisplayType(this.residueDisplay);
+                       residue = new DefaultMutableTreeNode(res);
+                       residuePath = treePath.pathByAddingChild(residue);
+                       res.setUserData(residuePath);
+                       treeModel.add(residue);
+               }
+       }       
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java
new file mode 100644 (file)
index 0000000..ec956d7
--- /dev/null
@@ -0,0 +1,957 @@
+package ext.edu.ucsf.rbvi.strucviz2;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This object maintains the relationship between Chimera objects and Cytoscape
+ * objects.
+ */
+
+public class StructureManager
+{
+  static final String[] defaultStructureKeys =
+  { "Structure", "pdb", "pdbFileName", "PDB ID", "structure",
+      "biopax.xref.PDB", "pdb_ids", "ModelName", "ModelNumber" };
+
+  static final String[] defaultChemStructKeys =
+  { "Smiles", "smiles", "SMILES" };
+
+  static final String[] defaultResidueKeys =
+  { "FunctionalResidues", "ResidueList", "Residues" };
+
+  private final String chimeraPropertyName = "chimera";
+
+  private final String chimeraPathPropertyKey = "LastChimeraPath";
+
+  public enum ModelType
+  {
+    PDB_MODEL, MODBASE_MODEL, SMILES
+  };
+
+  public static Properties pathProps;
+
+  private String chimeraCommandAttr = "ChimeraCommand";
+
+  private String chimeraOutputTable = "ChimeraTable";
+
+  private String chimeraOutputAttr = "ChimeraOutput";
+
+  private boolean haveGUI = true;
+
+  private ChimeraManager chimeraManager = null;
+
+  static private List<ChimeraStructuralObject> chimSelectionList;
+
+  private boolean ignoreCySelection = false;
+
+  private File configurationDirectory = null;
+
+  private static Logger logger = LoggerFactory
+          .getLogger(ext.edu.ucsf.rbvi.strucviz2.StructureManager.class);
+
+  public StructureManager(boolean haveGUI)
+  {
+    this.haveGUI = haveGUI;
+    // Create the Chimera interface
+    chimeraManager = new ChimeraManager(this);
+    chimSelectionList = new ArrayList<ChimeraStructuralObject>();
+    pathProps = new Properties();
+  }
+
+  public ChimeraManager getChimeraManager()
+  {
+    return chimeraManager;
+  }
+
+  public boolean openStructures(Collection<List<String>> chimObjNames,
+          ModelType type)
+  {
+    // new models
+    Map<String, List<ChimeraModel>> newModels = new HashMap<String, List<ChimeraModel>>();
+    if (chimObjNames.size() > 0)
+    {
+      List<String> names = chimObjNames.iterator().next();
+      if (names == null)
+      {
+        return false;
+      }
+      for (String chimObjName : names)
+      {
+        // get or open the corresponding models if they already exist
+        List<ChimeraModel> currentModels = chimeraManager.getChimeraModels(
+                chimObjName, type);
+        if (currentModels.size() == 0)
+        {
+          // open and return models
+          currentModels = chimeraManager.openModel(chimObjName, type);
+          if (currentModels == null)
+          {
+            // failed to open model, continue with next
+            continue;
+          }
+          // if (type == ModelType.SMILES) {
+          // newModels.put("smiles:" + chimObjName, currentModels);
+          // } else {
+          newModels.put(chimObjName, currentModels);
+          // }
+          // for each model
+          for (ChimeraModel currentModel : currentModels)
+          {
+            // if not RIN then associate new model with the Cytoscape
+            // node
+            // if (!currentChimMap.containsKey(currentModel)) {
+            // currentChimMap.put(currentModel, new HashSet<CyIdentifiable>());
+            // }
+          }
+        }
+      }
+    }
+    else
+    {
+      return false;
+    }
+    // update dialog
+    // if (mnDialog != null) {
+    // mnDialog.modelChanged();
+    // }
+    // aTask.associate();
+    return true;
+
+  }
+
+  // TODO: [Release] Handle case where one network is associated with two models
+  // that are opened
+  // at the same time
+  /*
+   * public boolean openStructures(CyNetwork network, Map<CyIdentifiable,
+   * List<String>> chimObjNames, ModelType type) { if
+   * (!chimeraManager.isChimeraLaunched() &&
+   * !chimeraManager.launchChimera(getChimeraPaths(network))) {
+   * logger.error("Chimera could not be launched."); return false; } else if
+   * (chimObjNames.size() == 0) { return false; } else if (network == null) {
+   * return openStructures(chimObjNames.values(), type); }
+   * 
+   * // potential rins Set<CyNetwork> potentialRINs = new HashSet<CyNetwork>();
+   * // attributes List<String> attrsFound = new ArrayList<String>();
+   * attrsFound.
+   * addAll(CytoUtils.getMatchingAttributes(network.getDefaultNodeTable(),
+   * getCurrentStructureKeys(network)));
+   * attrsFound.addAll(CytoUtils.getMatchingAttributes
+   * (network.getDefaultNodeTable(), getCurrentChemStructKeys(network))); // new
+   * models Map<String, List<ChimeraModel>> newModels = new HashMap<String,
+   * List<ChimeraModel>>(); // for each node that has an associated structure
+   * for (CyIdentifiable cyObj : chimObjNames.keySet()) { // get possible res
+   * specs List<String> specsFound = null; if (cyObj instanceof CyNode) {
+   * specsFound = ChimUtils.getResidueKeys(network.getDefaultNodeTable(), cyObj,
+   * attrsFound); } // save node to track its selection and mapping to chimera
+   * objects if (!currentCyMap.containsKey(cyObj)) { currentCyMap.put(cyObj, new
+   * HashSet<ChimeraStructuralObject>()); } // save node to network mapping to
+   * keep track of selection events if (!networkMap.containsKey(cyObj)) {
+   * networkMap.put(cyObj, new HashSet<CyNetwork>()); }
+   * networkMap.get(cyObj).add(network); // for each structure that has to be
+   * opened for (String chimObjName : chimObjNames.get(cyObj)) { // get or open
+   * the corresponding models if they already exist List<ChimeraModel>
+   * currentModels = chimeraManager.getChimeraModels(chimObjName, type); if
+   * (currentModels.size() == 0) { // open and return models currentModels =
+   * chimeraManager.openModel(chimObjName, type); if (currentModels == null) {
+   * // failed to open model, continue with next continue; } // if (type ==
+   * ModelType.SMILES) { // newModels.put("smiles:" + chimObjName,
+   * currentModels); // } else { newModels.put(chimObjName, currentModels); // }
+   * // for each model for (ChimeraModel currentModel : currentModels) { //
+   * check if it is a RIN boolean foundRIN = false; if
+   * (currentModel.getModelType().equals(ModelType.PDB_MODEL)) { // go through
+   * all node annotations and check if any of them is a residue // or a chain if
+   * (cyObj instanceof CyNode && network.containsNode((CyNode) cyObj) &&
+   * specsFound != null && specsFound.size() > 0) { for (String resSpec :
+   * specsFound) { ChimeraStructuralObject res =
+   * ChimUtils.fromAttribute(resSpec, chimeraManager); if (res != null && (res
+   * instanceof ChimeraResidue || res instanceof ChimeraChain)) { // if so,
+   * assume it might be a RIN potentialRINs.add(network); foundRIN = true;
+   * break; } } } else if (cyObj instanceof CyNetwork) { // if cyObj is a
+   * network, check for residue/chain annotations in an // arbitrary node
+   * CyNetwork rinNet = (CyNetwork) cyObj; if (rinNet.getNodeList().size() > 0)
+   * { specsFound = ChimUtils.getResidueKeys( rinNet.getDefaultNodeTable(),
+   * rinNet.getNodeList().get(0), attrsFound); for (String resSpec : specsFound)
+   * { ChimeraStructuralObject res = ChimUtils.fromAttribute( resSpec,
+   * chimeraManager); if (res != null && (res instanceof ChimeraResidue || res
+   * instanceof ChimeraChain)) { potentialRINs.add(network); foundRIN = true;
+   * break; } } } } } if (foundRIN) { continue; } // if not RIN then associate
+   * new model with the Cytoscape // node if
+   * (!currentChimMap.containsKey(currentModel)) {
+   * currentChimMap.put(currentModel, new HashSet<CyIdentifiable>()); } String
+   * cyObjName = network.getRow(cyObj).get(CyNetwork.NAME, String.class); if
+   * (cyObjName != null && cyObjName.endsWith(currentModel.getModelName())) { //
+   * it is a modbase model, associate directly
+   * currentCyMap.get(cyObj).add(currentModel);
+   * currentChimMap.get(currentModel).add(cyObj);
+   * currentModel.addCyObject(cyObj, network); } else if (specsFound != null &&
+   * specsFound.size() > 0) { for (String resSpec : specsFound) {
+   * ChimeraStructuralObject specModel = ChimUtils.fromAttribute( resSpec,
+   * chimeraManager); if (specModel == null &&
+   * resSpec.equals(currentModel.getModelName())) { specModel =
+   * chimeraManager.getChimeraModel( currentModel.getModelNumber(),
+   * currentModel.getSubModelNumber()); } if (specModel != null &&
+   * currentModel.toSpec().equals(specModel.toSpec()) ||
+   * currentModel.getModelName().equals("smiles:" + resSpec)) {
+   * currentCyMap.get(cyObj).add(currentModel);
+   * currentChimMap.get(currentModel).add(cyObj);
+   * currentModel.addCyObject(cyObj, network);
+   * currentModel.setFuncResidues(ChimUtils.parseFuncRes(
+   * getResidueList(network, cyObj), chimObjName)); } } } } } } } // networks
+   * that contain nodes associated to newly opened models // this will usually
+   * be of length 1 for (CyNetwork net : potentialRINs) {
+   * addStructureNetwork(net); } // update dialog if (mnDialog != null) {
+   * mnDialog.modelChanged(); } aTask.associate(); return true; }
+   */
+  public void closeStructures(Set<String> chimObjNames)
+  {
+    // for each cytoscape object and chimera model pair
+    for (String modelName : chimObjNames)
+    {
+      List<ChimeraModel> models = chimeraManager
+              .getChimeraModels(modelName);
+      for (ChimeraModel model : models)
+      {
+        closeModel(model);
+      }
+    }
+    // if (mnDialog != null) {
+    // mnDialog.modelChanged();
+    // }
+  }
+
+  // TODO: [Optional] Can we make a screenshot of a single molecule?
+  public File saveChimeraImage()
+  {
+    File tmpFile = null;
+    try
+    {
+      // Create the temp file name
+      tmpFile = File.createTempFile("structureViz", ".png");
+      chimeraManager.sendChimeraCommand("set bgTransparency", false);
+      chimeraManager.sendChimeraCommand(
+              "copy file " + tmpFile.getAbsolutePath() + " png", true);
+      chimeraManager.sendChimeraCommand("unset bgTransparency", false);
+    } catch (IOException ioe)
+    {
+      // Log error
+      logger.error("Error writing image", ioe);
+    }
+    return tmpFile;
+  }
+
+  public void closeModel(ChimeraModel model)
+  {
+    // close model in Chimera
+    chimeraManager.closeModel(model);
+    // remove all associations
+    // if (currentChimMap.containsKey(model)) {
+    // for (CyIdentifiable cyObj : model.getCyObjects().keySet()) {
+    // if (cyObj == null) {
+    // continue;
+    // } else if (currentCyMap.containsKey(cyObj)) {
+    // currentCyMap.get(cyObj).remove(model);
+    // } else if (cyObj instanceof CyNetwork) {
+    // for (ChimeraResidue residue : model.getResidues()) {
+    // if (currentChimMap.containsKey(residue)) {
+    // for (CyIdentifiable cyObjRes : currentChimMap.get(residue)) {
+    // if (currentCyMap.containsKey(cyObjRes)) {
+    // currentCyMap.get(cyObjRes).remove(residue);
+    // }
+    // }
+    // currentChimMap.remove(residue);
+    // }
+    // }
+    // }
+    // }
+    // currentChimMap.remove(model);
+    // }
+  }
+
+  // public void addStructureNetwork(CyNetwork rin) {
+  // if (rin == null) {
+  // return;
+  // }
+  // ChimeraModel model = null;
+  // // the network is not added to the model in the currentChimMap
+  // List<String> attrsFound =
+  // CytoUtils.getMatchingAttributes(rin.getDefaultNodeTable(),
+  // getCurrentStructureKeys(rin));
+  // for (CyNode node : rin.getNodeList()) {
+  // if (!networkMap.containsKey(node)) {
+  // networkMap.put(node, new HashSet<CyNetwork>());
+  // }
+  // networkMap.get(node).add(rin);
+  // List<String> specsFound =
+  // ChimUtils.getResidueKeys(rin.getDefaultNodeTable(), node,
+  // attrsFound);
+  // for (String residueSpec : specsFound) {
+  // // if (!rin.getRow(node).isSet(ChimUtils.RESIDUE_ATTR)) {
+  // // continue;
+  // // }
+  // // String residueSpec = rin.getRow(node).get(ChimUtils.RESIDUE_ATTR,
+  // String.class);
+  // ChimeraStructuralObject chimObj = ChimUtils.fromAttribute(residueSpec,
+  // chimeraManager);
+  // // chimObj.getChimeraModel().addCyObject(node, rin);
+  // if (chimObj == null || chimObj instanceof ChimeraModel) {
+  // continue;
+  // }
+  // model = chimObj.getChimeraModel();
+  // if (!currentCyMap.containsKey(node)) {
+  // currentCyMap.put(node, new HashSet<ChimeraStructuralObject>());
+  // }
+  // currentCyMap.get(node).add(chimObj);
+  // if (!currentChimMap.containsKey(chimObj)) {
+  // currentChimMap.put(chimObj, new HashSet<CyIdentifiable>());
+  // }
+  // currentChimMap.get(chimObj).add(node);
+  // }
+  // }
+  // if (model != null) {
+  // model.addCyObject(rin, rin);
+  // if (!currentCyMap.containsKey(rin)) {
+  // currentCyMap.put(rin, new HashSet<ChimeraStructuralObject>());
+  // }
+  // currentCyMap.get(rin).add(model);
+  // }
+  // }
+
+  public void exitChimera()
+  {
+    // // exit chimera, invokes clearOnExitChimera
+    // if (mnDialog != null) {
+    // mnDialog.setVisible(false);
+    // mnDialog = null;
+    // }
+    // if (alDialog != null) {
+    // alDialog.setVisible(false);
+    // }
+    chimeraManager.exitChimera();
+  }
+
+  // invoked by ChimeraManager whenever Chimera exits
+  public void clearOnChimeraExit()
+  {
+    // // clear structures
+    // currentCyMap.clear();
+    // currentChimMap.clear();
+    // networkMap.clear();
+    chimSelectionList.clear();
+    // if (chimTable != null) {
+    // ((CyTableManager)
+    // getService(CyTableManager.class)).deleteTable(chimTable.getSUID());
+    // }
+    // if (mnDialog != null) {
+    // if (mnDialog.isVisible()) {
+    // mnDialog.lostChimera();
+    // mnDialog.setVisible(false);
+    // }
+    // mnDialog = null;
+    // if (alDialog != null) {
+    // alDialog.setVisible(false);
+    // }
+    // }
+  }
+
+  // We need to do this in two passes since some parts of a structure might be
+  // selected and some might not. Our selection model (unfortunately) only
+  // tells
+  // us that something has changed, not what...
+  public void updateCytoscapeSelection()
+  {
+    // List<ChimeraStructuralObject> selectedChimObj
+    ignoreCySelection = true;
+    // System.out.println("update Cytoscape selection");
+    // find all possibly selected Cytoscape objects and unselect them
+    // Set<CyNetwork> networks = new HashSet<CyNetwork>();
+    // for (CyIdentifiable currentCyObj : currentCyMap.keySet()) {
+    // if (!networkMap.containsKey(currentCyObj)) {
+    // continue;
+    // }
+    // Set<CyNetwork> currentCyNetworks = networkMap.get(currentCyObj);
+    // if (currentCyNetworks == null || currentCyNetworks.size() == 0) {
+    //
+    // continue;
+    // }
+    // for (CyNetwork network : currentCyNetworks) {
+    // if ((currentCyObj instanceof CyNode && network.containsNode((CyNode)
+    // currentCyObj))
+    // || (currentCyObj instanceof CyEdge && network
+    // .containsEdge((CyEdge) currentCyObj))) {
+    // network.getRow(currentCyObj).set(CyNetwork.SELECTED, false);
+    // networks.add(network);
+    // }
+    // }
+    // }
+    //
+    // // select only those associated with selected Chimera objects
+    // Set<CyIdentifiable> currentCyObjs = new HashSet<CyIdentifiable>();
+    // for (ChimeraStructuralObject chimObj : chimSelectionList) {
+    // ChimeraModel currentSelModel = chimObj.getChimeraModel();
+    // if (currentChimMap.containsKey(currentSelModel)) {
+    // currentCyObjs.addAll(currentChimMap.get(currentSelModel));
+    // }
+    // if (currentChimMap.containsKey(chimObj)) {
+    // currentCyObjs.addAll(currentChimMap.get(chimObj));
+    // }
+    // // System.out.println(chimObj.toSpec() + ": " +
+    // // currentCyObjs.size());
+    // }
+    // for (CyIdentifiable cyObj : currentCyObjs) {
+    // // System.out.println(cyObj.toString());
+    // if (cyObj == null || !networkMap.containsKey(cyObj)) {
+    // continue;
+    // }
+    // Set<CyNetwork> currentCyNetworks = networkMap.get(cyObj);
+    // if (currentCyNetworks == null || currentCyNetworks.size() == 0) {
+    // continue;
+    // }
+    // for (CyNetwork network : currentCyNetworks) {
+    // if ((cyObj instanceof CyNode && network.containsNode((CyNode) cyObj))
+    // || (cyObj instanceof CyEdge && network.containsEdge((CyEdge) cyObj))) {
+    // network.getRow(cyObj).set(CyNetwork.SELECTED, true);
+    // networks.add(network);
+    // }
+    // }
+    // }
+    //
+    // CyNetworkViewManager cyNetViewManager = (CyNetworkViewManager)
+    // getService(CyNetworkViewManager.class);
+    // // Update network views
+    // for (CyNetwork network : networks) {
+    // Collection<CyNetworkView> views =
+    // cyNetViewManager.getNetworkViews(network);
+    // for (CyNetworkView view : views) {
+    // view.updateView();
+    // }
+    // }
+    ignoreCySelection = false;
+  }
+
+  public void cytoscapeSelectionChanged(Map<Long, Boolean> selectedRows)
+  {
+    // if (ignoreCySelection || currentCyMap.size() == 0) {
+    // return;
+    // }
+    // // clearSelectionList();
+    // // System.out.println("cytoscape selection changed");
+    // // iterate over all cy objects with associated models
+    // for (CyIdentifiable cyObj : currentCyMap.keySet()) {
+    // if (cyObj instanceof CyNetwork ||
+    // !selectedRows.containsKey(cyObj.getSUID())) {
+    // continue;
+    // }
+    // for (ChimeraStructuralObject chimObj : currentCyMap.get(cyObj)) {
+    // if (selectedRows.get(cyObj.getSUID())) {
+    // addChimSelection(chimObj);
+    // if (chimObj instanceof ChimeraResidue) {
+    // if (chimObj.getChimeraModel().isSelected()) {
+    // removeChimSelection(chimObj.getChimeraModel());
+    // } else if (chimObj.getChimeraModel()
+    // .getChain(((ChimeraResidue) chimObj).getChainId()).isSelected()) {
+    // removeChimSelection(chimObj.getChimeraModel().getChain(
+    // ((ChimeraResidue) chimObj).getChainId()));
+    // }
+    // }
+    // } else {
+    // removeChimSelection(chimObj);
+    // if (chimObj.hasSelectedChildren() && chimObj instanceof ChimeraModel) {
+    // for (ChimeraResidue residue : ((ChimeraModel) chimObj)
+    // .getSelectedResidues()) {
+    // removeChimSelection(residue);
+    // }
+    // }
+    // }
+    // }
+    // }
+    // System.out.println("selection list: " + getChimSelectionCount());
+    updateChimeraSelection();
+    selectionChanged();
+  }
+
+  // Save models in a HashMap/Set for better performance?
+  public void updateChimeraSelection()
+  {
+    // System.out.println("update Chimera selection");
+    String selSpec = "";
+    for (int i = 0; i < chimSelectionList.size(); i++)
+    {
+      ChimeraStructuralObject nodeInfo = chimSelectionList.get(i);
+      // we do not care about the model anymore
+      selSpec = selSpec.concat(nodeInfo.toSpec());
+      if (i < chimSelectionList.size() - 1)
+        selSpec.concat("|");
+    }
+    if (selSpec.length() > 0)
+    {
+      chimeraManager.select("sel " + selSpec);
+    }
+    else
+    {
+      chimeraManager.select("~sel");
+    }
+  }
+
+  /**
+   * This is called by the selectionListener to let us know that the user has
+   * changed their selection in Chimera. We need to go back to Chimera to find
+   * out what is currently selected and update our list.
+   */
+  public void chimeraSelectionChanged()
+  {
+    // System.out.println("Chimera selection changed");
+    clearSelectionList();
+    // Execute the command to get the list of models with selections
+    Map<Integer, ChimeraModel> selectedModelsMap = chimeraManager
+            .getSelectedModels();
+    // Now get the residue-level data
+    chimeraManager.getSelectedResidues(selectedModelsMap);
+    // Get the selected objects
+    try
+    {
+      for (ChimeraModel selectedModel : selectedModelsMap.values())
+      {
+        int modelNumber = selectedModel.getModelNumber();
+        int subModelNumber = selectedModel.getSubModelNumber();
+        // Get the corresponding "real" model
+        if (chimeraManager.hasChimeraModel(modelNumber, subModelNumber))
+        {
+          ChimeraModel dataModel = chimeraManager.getChimeraModel(
+                  modelNumber, subModelNumber);
+          if (dataModel.getResidueCount() == selectedModel
+                  .getResidueCount()
+                  || dataModel.getModelType() == StructureManager.ModelType.SMILES)
+          {
+            // Select the entire model
+            addChimSelection(dataModel);
+            // dataModel.setSelected(true);
+          }
+          else
+          {
+            for (ChimeraChain selectedChain : selectedModel.getChains())
+            {
+              ChimeraChain dataChain = dataModel.getChain(selectedChain
+                      .getChainId());
+              if (selectedChain.getResidueCount() == dataChain
+                      .getResidueCount())
+              {
+                addChimSelection(dataChain);
+                // dataChain.setSelected(true);
+              }
+              // else {
+              // Need to select individual residues
+              for (ChimeraResidue res : selectedChain.getResidues())
+              {
+                String residueIndex = res.getIndex();
+                ChimeraResidue residue = dataChain.getResidue(residueIndex);
+                if (residue == null)
+                {
+                  continue;
+                }
+                addChimSelection(residue);
+                // residue.setSelected(true);
+              } // resIter.hasNext
+                // }
+            } // chainIter.hasNext()
+          }
+        }
+      } // modelIter.hasNext()
+    } catch (Exception ex)
+    {
+      logger.warn("Could not update selection", ex);
+    }
+    // System.out.println("selection list: " + getChimSelectionCount());
+    // Finally, update the navigator panel
+    selectionChanged();
+    updateCytoscapeSelection();
+  }
+
+  public void selectFunctResidues(Collection<ChimeraModel> models)
+  {
+    clearSelectionList();
+    for (ChimeraModel model : models)
+    {
+      for (ChimeraResidue residue : model.getFuncResidues())
+      {
+        addChimSelection(residue);
+      }
+    }
+    updateChimeraSelection();
+    updateCytoscapeSelection();
+    selectionChanged();
+  }
+
+  // public void selectFunctResidues(CyNode node, CyNetwork network) {
+  // clearSelectionList();
+  // if (currentCyMap.containsKey(node)) {
+  // Set<ChimeraStructuralObject> chimObjects = currentCyMap.get(node);
+  // for (ChimeraStructuralObject obj : chimObjects) {
+  // if (obj instanceof ChimeraModel) {
+  // ChimeraModel model = (ChimeraModel) obj;
+  // for (ChimeraResidue residue : model.getFuncResidues()) {
+  // addChimSelection(residue);
+  // }
+  // }
+  // }
+  // }
+  // updateChimeraSelection();
+  // updateCytoscapeSelection();
+  // selectionChanged();
+  // }
+
+  public List<ChimeraStructuralObject> getChimSelectionList()
+  {
+    return chimSelectionList;
+  }
+
+  public int getChimSelectionCount()
+  {
+    return chimSelectionList.size();
+  }
+
+  /**
+   * Add a selection to the selection list. This is called primarily by the
+   * Model Navigator Dialog to keep the selections in sync
+   * 
+   * @param selectionToAdd
+   *          the selection to add to our list
+   */
+  public void addChimSelection(ChimeraStructuralObject selectionToAdd)
+  {
+    if (selectionToAdd != null
+            && !chimSelectionList.contains(selectionToAdd))
+    {
+      chimSelectionList.add(selectionToAdd);
+      selectionToAdd.setSelected(true);
+    }
+  }
+
+  /**
+   * Remove a selection from the selection list. This is called primarily by the
+   * Model Navigator Dialog to keep the selections in sync
+   * 
+   * @param selectionToRemove
+   *          the selection to remove from our list
+   */
+  public void removeChimSelection(ChimeraStructuralObject selectionToRemove)
+  {
+    if (selectionToRemove != null
+            && chimSelectionList.contains(selectionToRemove))
+    {
+      chimSelectionList.remove(selectionToRemove);
+      selectionToRemove.setSelected(false);
+    }
+  }
+
+  /**
+   * Clear the list of selected objects
+   */
+  public void clearSelectionList()
+  {
+    for (ChimeraStructuralObject cso : chimSelectionList)
+    {
+      if (cso != null)
+        cso.setSelected(false);
+    }
+    chimSelectionList.clear();
+  }
+
+  /**
+   * Associate a new network with the corresponding Chimera objects.
+   * 
+   * @param network
+   */
+
+  /**
+   * Dump and refresh all of our model/chain/residue info
+   */
+  public void updateModels()
+  {
+    // Stop all of our listeners while we try to handle this
+    chimeraManager.stopListening();
+
+    // Get all of the open models
+    List<ChimeraModel> newModelList = chimeraManager.getModelList();
+
+    // Match them up -- assume that the model #'s haven't changed
+    for (ChimeraModel newModel : newModelList)
+    {
+      // Get the color (for our navigator)
+      newModel.setModelColor(chimeraManager.getModelColor(newModel));
+
+      // Get our model info
+      int modelNumber = newModel.getModelNumber();
+      int subModelNumber = newModel.getSubModelNumber();
+
+      // If we already know about this model number, get the Structure,
+      // which tells us about the associated CyNode
+      if (chimeraManager.hasChimeraModel(modelNumber, subModelNumber))
+      {
+        ChimeraModel oldModel = chimeraManager.getChimeraModel(modelNumber,
+                subModelNumber);
+        chimeraManager.removeChimeraModel(modelNumber, subModelNumber);
+        newModel.setModelType(oldModel.getModelType());
+        if (oldModel.getModelType() == ModelType.SMILES)
+        {
+          newModel.setModelName(oldModel.getModelName());
+        }
+        // re-assign associations to cytoscape objects
+        // Map<CyIdentifiable, CyNetwork> oldModelCyObjs =
+        // oldModel.getCyObjects();
+        // for (CyIdentifiable cyObj : oldModelCyObjs.keySet()) {
+        // // add cy objects to the new model
+        // newModel.addCyObject(cyObj, oldModelCyObjs.get(cyObj));
+        // if (currentCyMap.containsKey(cyObj)) {
+        // currentCyMap.get(cyObj).add(newModel);
+        // if (currentCyMap.get(cyObj).contains(oldModel)) {
+        // currentCyMap.get(cyObj).remove(oldModel);
+        // }
+        // }
+        // }
+        // // add new model to the chimera objects map and remove old model
+        // if (currentChimMap.containsKey(oldModel)) {
+        // currentChimMap.put(newModel, currentChimMap.get(oldModel));
+        // currentChimMap.remove(oldModel);
+        // }
+      }
+      // add new model to ChimeraManager
+      chimeraManager.addChimeraModel(modelNumber, subModelNumber, newModel);
+
+      // Get the residue information
+      if (newModel.getModelType() != ModelType.SMILES)
+      {
+        chimeraManager.addResidues(newModel);
+      }
+      // for (CyIdentifiable cyObj : newModel.getCyObjects().keySet()) {
+      // if (cyObj != null && cyObj instanceof CyNetwork) {
+      // addStructureNetwork((CyNetwork) cyObj);
+      // } else if (cyObj != null && cyObj instanceof CyNode) {
+      // newModel.setFuncResidues(ChimUtils.parseFuncRes(
+      // getResidueList(newModel.getCyObjects().get(cyObj), cyObj),
+      // newModel.getModelName()));
+      // }
+      // }
+    }
+
+    // associate all models with any node or network
+    // aTask.associate();
+
+    // Restart all of our listeners
+    chimeraManager.startListening();
+    // Done
+  }
+
+  public void launchModelNavigatorDialog()
+  {
+    // TODO: [Optional] Use haveGUI flag
+    // if (!haveGUI) {
+    // return;
+    // }
+    // if (mnDialog == null) {
+    // CySwingApplication cyApplication = (CySwingApplication)
+    // getService(CySwingApplication.class);
+    // mnDialog = new ModelNavigatorDialog(cyApplication.getJFrame(), this);
+    // mnDialog.pack();
+    // }
+    // mnDialog.setVisible(true);
+  }
+
+  public boolean isMNDialogOpen()
+  {
+    // if (mnDialog != null && mnDialog.isVisible()) {
+    // return true;
+    // }
+    return false;
+  }
+
+  /**
+   * Invoked by the listener thread.
+   */
+  public void modelChanged()
+  {
+    // if (mnDialog != null) {
+    // mnDialog.modelChanged();
+    // }
+  }
+
+  /**
+   * Inform our interface that the selection has changed
+   */
+  public void selectionChanged()
+  {
+    // if (mnDialog != null) {
+    // // System.out.println("update dialog selection");
+    // mnDialog.updateSelection(new
+    // ArrayList<ChimeraStructuralObject>(chimSelectionList));
+    // }
+  }
+
+  public void launchAlignDialog(boolean useChains)
+  {
+    // TODO: [Optional] Use haveGUI flag
+    // Sometimes it does not appear in Windows
+    // if (!haveGUI) {
+    // return;
+    // }
+    // if (alDialog != null) {
+    // alDialog.setVisible(false);
+    // alDialog.dispose();
+    // }
+    // System.out.println("launch align dialog");
+    List<ChimeraStructuralObject> chimObjectList = new ArrayList<ChimeraStructuralObject>();
+    for (ChimeraModel model : chimeraManager.getChimeraModels())
+    {
+      if (useChains)
+      {
+        for (ChimeraChain chain : model.getChains())
+        {
+          chimObjectList.add(chain);
+        }
+      }
+      else
+      {
+        chimObjectList.add(model);
+      }
+    }
+    // Bring up the dialog
+    // CySwingApplication cyApplication = (CySwingApplication)
+    // getService(CySwingApplication.class);
+    // alDialog = new AlignStructuresDialog(cyApplication.getJFrame(), this,
+    // chimObjectList);
+    // alDialog.pack();
+    // alDialog.setVisible(true);
+  }
+
+  public List<String> getAllStructureKeys()
+  {
+    return Arrays.asList(defaultStructureKeys);
+  }
+
+  public List<String> getAllChemStructKeys()
+  {
+    return Arrays.asList(defaultChemStructKeys);
+  }
+
+  public List<String> getAllResidueKeys()
+  {
+    return Arrays.asList(defaultResidueKeys);
+  }
+
+  public List<String> getAllChimeraResidueAttributes()
+  {
+    List<String> attributes = new ArrayList<String>();
+    // attributes.addAll(rinManager.getResAttrs());
+    attributes.addAll(chimeraManager.getAttrList());
+    return attributes;
+  }
+
+  StructureSettings defaultSettings = null;
+
+  // TODO: [Optional] Change priority of Chimera paths
+  public List<String> getChimeraPaths()
+  {
+    List<String> pathList = new ArrayList<String>();
+
+    // if no network is available and the settings have been modified by the
+    // user, check for a
+    // path to chimera
+    if (defaultSettings != null)
+    {
+      String defaultPath = defaultSettings.getChimeraPath();
+      if (defaultPath != null && !defaultPath.equals(""))
+      {
+        pathList.add(defaultPath);
+        return pathList;
+      }
+    }
+
+    // if no network settings, check if the last chimera path is saved in the
+    // session
+    // String lastPath = CytoUtils.getDefaultChimeraPath(registrar,
+    // chimeraPropertyName,
+    // chimeraPathPropertyKey);
+    // if (lastPath != null && !lastPath.equals("")) {
+    // pathList.add(lastPath);
+    // return pathList;
+    // }
+
+    // if no user settings and no last path, get default system's settings
+    String os = System.getProperty("os.name");
+    if (os.startsWith("Linux"))
+    {
+      pathList.add("/usr/local/chimera/bin/chimera");
+      pathList.add("/usr/local/bin/chimera");
+      pathList.add("/usr/bin/chimera");
+    }
+    else if (os.startsWith("Windows"))
+    {
+      pathList.add("\\Program Files\\Chimera\\bin\\chimera");
+      pathList.add("C:\\Program Files\\Chimera\\bin\\chimera.exe");
+    }
+    else if (os.startsWith("Mac"))
+    {
+      pathList.add("/Applications/Chimera.app/Contents/MacOS/chimera");
+    }
+    return pathList;
+  }
+
+  public void setChimeraPathProperty(String path)
+  {
+    // CytoUtils.setDefaultChimeraPath(registrar, chimeraPropertyName,
+    // chimeraPathPropertyKey,
+    // path);
+  }
+
+  public void setStructureSettings(StructureSettings structureSettings)
+  {
+    this.defaultSettings = structureSettings;
+  }
+
+  public String getCurrentChimeraPath(Object object)
+  {
+    if (defaultSettings != null)
+    {
+      return defaultSettings.getChimeraPath();
+    }
+    else
+    {
+      return "";
+    }
+  }
+
+  // public void initChimTable() {
+  // CyTableManager manager = (CyTableManager) getService(CyTableManager.class);
+  // CyTableFactory factory = (CyTableFactory) getService(CyTableFactory.class);
+  // for (CyTable table : manager.getGlobalTables()) {
+  // if (table.getTitle().equals(chimeraOutputTable)) {
+  // manager.deleteTable(table.getSUID());
+  // }
+  // }
+  // chimTable = factory.createTable(chimeraOutputTable, chimeraCommandAttr,
+  // String.class,
+  // false, true);
+  // manager.addTable(chimTable);
+  // if (chimTable.getColumn(chimeraOutputAttr) == null) {
+  // chimTable.createListColumn(chimeraOutputAttr, String.class, false);
+  // }
+  // }
+
+  // public void addChimReply(String command, List<String> reply) {
+  // chimTable.getRow(command).set(chimeraOutputAttr, reply);
+  // }
+
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/StructureSettings.java b/src/ext/edu/ucsf/rbvi/strucviz2/StructureSettings.java
new file mode 100644 (file)
index 0000000..161ab13
--- /dev/null
@@ -0,0 +1,25 @@
+package ext.edu.ucsf.rbvi.strucviz2;
+
+/**
+ * This object maintains the relationship between Chimera objects and Cytoscape objects.
+ */
+public class StructureSettings {
+
+//     @Tunable(description = "Path to UCSF Chimera application", gravity = 4.0)
+       public String chimeraPath = null;
+
+       public StructureSettings(StructureManager manager) {
+               
+               chimeraPath = manager.getCurrentChimeraPath(null);
+
+               // This seems a little strange, but it has to do with the order of tunable interceptor
+               // handling. We need to set these selectors in our structure manager and dynamically
+               // pull the data out as needed....
+               manager.setStructureSettings( this);
+       }
+
+
+       public String getChimeraPath() {
+               return chimeraPath;
+       }
+}
diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java b/src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java
new file mode 100644 (file)
index 0000000..8391e49
--- /dev/null
@@ -0,0 +1,217 @@
+package ext.edu.ucsf.rbvi.strucviz2.port;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ext.edu.ucsf.rbvi.strucviz2.*;
+
+/***************************************************
+ *                 Thread Classes                  *
+ **************************************************/
+
+/**
+ * Reply listener thread
+ */
+public class ListenerThreads extends Thread {
+       private InputStream readChan = null;
+       private BufferedReader lineReader = null;
+       private Process chimera = null;
+       private Map<String, List<String>> replyLog = null;
+       private Logger logger;
+       private StructureManager structureManager = null;
+
+       /**
+        * Create a new listener thread to read the responses from Chimera
+        * 
+        * @param chimera
+        *            a handle to the Chimera Process
+        * @param log
+        *            a handle to a List to post the responses to
+        * @param chimeraObject
+        *            a handle to the Chimera Object
+        */
+       public ListenerThreads(Process chimera, StructureManager structureManager) {
+               this.chimera = chimera;
+               this.structureManager = structureManager;
+               replyLog = new HashMap<String, List<String>>();
+               // Get a line-oriented reader
+               readChan = chimera.getInputStream();
+               lineReader = new BufferedReader(new InputStreamReader(readChan));
+               logger = LoggerFactory.getLogger(ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads.class);
+       }
+
+       /**
+        * Start the thread running
+        */
+       public void run() {
+               // System.out.println("ReplyLogListener running");
+               while (true) {
+                       try {
+                               chimeraRead();
+                       } catch (IOException e) {
+                               logger.warn("UCSF Chimera has exited: " + e.getMessage());
+                               return;
+                       }
+               }
+       }
+
+       public List<String> getResponse(String command) {
+               List<String> reply;
+               // System.out.println("getResponse: "+command);
+               while (!replyLog.containsKey(command)) {
+                       try {
+                               Thread.currentThread().sleep(100);
+                       } catch (InterruptedException e) {
+                       }
+               }
+
+               synchronized (replyLog) {
+                       reply = replyLog.get(command);
+                       // System.out.println("getResponse ("+command+") = "+reply);
+                       replyLog.remove(command);
+               }
+               return reply;
+       }
+
+       public void clearResponse(String command) {
+               try {
+                       Thread.currentThread().sleep(100);
+               } catch (InterruptedException e) {
+               }
+               if (replyLog.containsKey(command))
+                       replyLog.remove(command);
+               return;
+       }
+
+       /**
+        * Read input from Chimera
+        * 
+        * @return a List containing the replies from Chimera
+        */
+       private void chimeraRead() throws IOException {
+               if (chimera == null)
+                       return;
+
+               String line = null;
+               while ((line = lineReader.readLine()) != null) {
+                       // System.out.println("From Chimera-->" + line);
+                       if (line.startsWith("CMD")) {
+                               chimeraCommandRead(line.substring(4));
+                       } else if (line.startsWith("ModelChanged: ")) {
+                               (new ModelUpdater()).start();
+                       } else if (line.startsWith("SelectionChanged: ")) {
+                               (new SelectionUpdater()).start();
+                       } else if (line.startsWith("Trajectory residue network info:")) {
+                               (new NetworkUpdater(line)).start();
+                       }
+               }
+               return;
+       }
+
+       private void chimeraCommandRead(String command) throws IOException {
+               // Generally -- looking for:
+               // CMD command
+               // ........
+               // END
+               // We return the text in between
+               List<String> reply = new ArrayList<String>();
+               boolean updateModels = false;
+               boolean updateSelection = false;
+               boolean importNetwork = false;
+               String line = null;
+
+               synchronized (replyLog) {
+                       while ((line = lineReader.readLine()) != null) {
+                               // System.out.println("From Chimera (" + command + ") -->" + line);
+                               if (line.startsWith("CMD")) {
+                                       logger.warn("Got unexpected command from Chimera: " + line);
+
+                               } else if (line.startsWith("END")) {
+                                       break;
+                               }
+                               if (line.startsWith("ModelChanged: ")) {
+                                       updateModels = true;
+                               } else if (line.startsWith("SelectionChanged: ")) {
+                                       updateSelection = true;
+                               } else if (line.length() == 0) {
+                                       continue;
+                               } else if (!line.startsWith("CMD")) {
+                                       reply.add(line);
+                               } else if (line.startsWith("Trajectory residue network info:")) {
+                                       importNetwork = true;
+                               }
+                       }
+                       replyLog.put(command, reply);
+               }
+               if (updateModels)
+                       (new ModelUpdater()).start();
+               if (updateSelection)
+                       (new SelectionUpdater()).start();
+               if (importNetwork) {
+                       (new NetworkUpdater(line)).start();
+               }
+               return;
+       }
+
+       /**
+        * Model updater thread
+        */
+       class ModelUpdater extends Thread {
+
+               public ModelUpdater() {
+               }
+
+               public void run() {
+                       structureManager.updateModels();
+                       structureManager.modelChanged();
+               }
+       }
+
+       /**
+        * Selection updater thread
+        */
+       class SelectionUpdater extends Thread {
+
+               public SelectionUpdater() {
+               }
+
+               public void run() {
+                       try {
+//                             structureManager.chimeraSelectionChanged();
+                       } catch (Exception e) {
+                               logger.warn("Could not update selection", e);
+                       }
+               }
+       }
+
+       /**
+        * Selection updater thread
+        */
+       class NetworkUpdater extends Thread {
+
+               private String line;
+
+               public NetworkUpdater(String line) {
+                       this.line = line;
+               }
+
+               public void run() {
+                       try {
+//                             ((TaskManager<?, ?>) structureManager.getService(TaskManager.class))
+//                                             .execute(new ImportTrajectoryRINTaskFactory(structureManager, line)
+//                                                             .createTaskIterator());
+                       } catch (Exception e) {
+                               logger.warn("Could not import trajectory network", e);
+                       }
+               }
+       }
+}