1 package ext.edu.ucsf.rbvi.strucviz2;
3 import jalview.ws.HttpClientUtils;
6 import java.io.BufferedReader;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.io.InputStreamReader;
11 import java.util.ArrayList;
12 import java.util.Collection;
13 import java.util.HashMap;
14 import java.util.List;
17 import org.apache.http.NameValuePair;
18 import org.apache.http.message.BasicNameValuePair;
19 import org.slf4j.Logger;
20 import org.slf4j.LoggerFactory;
22 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
23 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
26 * This object maintains the Chimera communication information.
28 public class ChimeraManager
30 private static final boolean debug = false;
33 * true: use REST API (recommended), false: use stdout/stdin (deprecated)
35 // TODO remove once definitely happy with using REST
36 private static final boolean USE_REST = true;
38 private int chimeraRestPort;
40 private Process chimera;
42 private ListenerThreads chimeraListenerThread;
44 private Map<Integer, ChimeraModel> currentModelsMap;
46 private Logger logger = LoggerFactory
47 .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
49 private StructureManager structureManager;
51 public ChimeraManager(StructureManager structureManager)
53 this.structureManager = structureManager;
55 chimeraListenerThread = null;
56 currentModelsMap = new HashMap<Integer, ChimeraModel>();
60 public List<ChimeraModel> getChimeraModels(String modelName)
62 List<ChimeraModel> models = getChimeraModels(modelName,
64 models.addAll(getChimeraModels(modelName, ModelType.SMILES));
68 public List<ChimeraModel> getChimeraModels(String modelName,
71 List<ChimeraModel> models = new ArrayList<ChimeraModel>();
72 for (ChimeraModel model : currentModelsMap.values())
74 if (modelName.equals(model.getModelName())
75 && modelType.equals(model.getModelType()))
83 public Map<String, List<ChimeraModel>> getChimeraModelsMap()
85 Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
86 for (ChimeraModel model : currentModelsMap.values())
88 String modelName = model.getModelName();
89 if (!models.containsKey(modelName))
91 models.put(modelName, new ArrayList<ChimeraModel>());
93 if (!models.get(modelName).contains(model))
95 models.get(modelName).add(model);
101 public ChimeraModel getChimeraModel(Integer modelNumber,
102 Integer subModelNumber)
104 Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
105 if (currentModelsMap.containsKey(key))
107 return currentModelsMap.get(key);
112 public ChimeraModel getChimeraModel()
114 return currentModelsMap.values().iterator().next();
117 public Collection<ChimeraModel> getChimeraModels()
119 // this method is invoked by the model navigator dialog
120 return currentModelsMap.values();
123 public int getChimeraModelsCount(boolean smiles)
125 // this method is invokes by the model navigator dialog
126 int counter = currentModelsMap.size();
132 for (ChimeraModel model : currentModelsMap.values())
134 if (model.getModelType() == ModelType.SMILES)
142 public boolean hasChimeraModel(Integer modelNubmer)
144 return hasChimeraModel(modelNubmer, 0);
147 public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
149 return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
153 public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
156 currentModelsMap.put(
157 ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
160 public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
162 int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
163 if (currentModelsMap.containsKey(modelKey))
165 currentModelsMap.remove(modelKey);
169 public List<ChimeraModel> openModel(String modelPath, ModelType type)
171 return openModel(modelPath, getFileNameFromPath(modelPath), type);
175 * Overloaded method to allow Jalview to pass in a model name.
182 public List<ChimeraModel> openModel(String modelPath, String modelName,
185 logger.info("chimera open " + modelPath);
187 List<String> response = null;
188 // TODO: [Optional] Handle modbase models
189 if (type == ModelType.MODBASE_MODEL)
191 response = sendChimeraCommand("open modbase:" + modelPath, true);
192 // } else if (type == ModelType.SMILES) {
193 // response = sendChimeraCommand("open smiles:" + modelName, true);
194 // modelName = "smiles:" + modelName;
198 response = sendChimeraCommand("open " + modelPath, true);
200 if (response == null)
202 // something went wrong
203 logger.warn("Could not open " + modelPath);
206 List<ChimeraModel> models = new ArrayList<ChimeraModel>();
207 int[] modelNumbers = null;
208 if (type == ModelType.PDB_MODEL)
210 for (String line : response)
212 if (line.startsWith("#"))
214 modelNumbers = ChimUtils.parseOpenedModelNumber(line);
215 if (modelNumbers != null)
217 int modelNumber = ChimUtils.makeModelKey(modelNumbers[0],
219 if (currentModelsMap.containsKey(modelNumber))
223 ChimeraModel newModel = new ChimeraModel(modelName, type,
224 modelNumbers[0], modelNumbers[1]);
225 currentModelsMap.put(modelNumber, newModel);
226 models.add(newModel);
227 // patch for Jalview - set model name in Chimera
228 sendChimeraCommand("setattr M name " + modelName + " #"
229 + modelNumbers[0], false);
230 // end patch for Jalview
238 // TODO: [Optional] Open smiles from file would fail. Do we need it?
239 // If parsing fails, iterate over all open models to get the right one
240 List<ChimeraModel> openModels = getModelList();
241 for (ChimeraModel openModel : openModels)
243 String openModelName = openModel.getModelName();
244 if (openModelName.endsWith("..."))
246 openModelName = openModelName.substring(0,
247 openModelName.length() - 3);
249 if (modelPath.startsWith(openModelName))
251 openModel.setModelName(modelPath);
252 int modelNumber = ChimUtils
253 .makeModelKey(openModel.getModelNumber(),
254 openModel.getSubModelNumber());
255 if (!currentModelsMap.containsKey(modelNumber))
257 currentModelsMap.put(modelNumber, openModel);
258 models.add(openModel);
264 // assign color and residues to open models
265 for (ChimeraModel newModel : models)
268 Color modelColor = getModelColor(newModel);
269 if (modelColor != null)
271 newModel.setModelColor(modelColor);
274 // Get our properties (default color scheme, etc.)
275 // Make the molecule look decent
276 // chimeraSend("repr stick "+newModel.toSpec());
278 // Create the information we need for the navigator
279 if (type != ModelType.SMILES)
281 addResidues(newModel);
285 sendChimeraCommand("focus", false);
291 * Refactored method to extract the last (or only) element delimited by file
297 private String getFileNameFromPath(String modelPath)
299 String modelName = modelPath;
300 if (modelPath == null)
304 // TODO: [Optional] Convert path to name in a better way
305 if (modelPath.lastIndexOf(File.separator) > 0)
307 modelName = modelPath.substring(modelPath
308 .lastIndexOf(File.separator) + 1);
310 else if (modelPath.lastIndexOf("/") > 0)
312 modelName = modelPath
313 .substring(modelPath.lastIndexOf("/") + 1);
318 public void closeModel(ChimeraModel model)
320 // int model = structure.modelNumber();
321 // int subModel = structure.subModelNumber();
322 // Integer modelKey = makeModelKey(model, subModel);
324 logger.info("chimera close model " + model.getModelName());
325 if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
326 model.getModelNumber(), model.getSubModelNumber())))
328 sendChimeraCommand("close " + model.toSpec(), false);
329 // currentModelNamesMap.remove(model.getModelName());
330 currentModelsMap.remove(ChimUtils.makeModelKey(
331 model.getModelNumber(), model.getSubModelNumber()));
332 // selectionList.remove(chimeraModel);
336 logger.warn("Could not find model " + model.getModelName()
342 public void startListening()
344 sendChimeraCommand("listen start models; listen start select", false);
347 public void stopListening()
349 sendChimeraCommand("listen stop models; listen stop select", false);
353 * Select something in Chimera
356 * the selection command to pass to Chimera
358 public void select(String command)
360 sendChimeraCommand("listen stop select; " + command
361 + "; listen start select", false);
366 sendChimeraCommand("focus", false);
369 public void clearOnChimeraExit()
372 currentModelsMap.clear();
375 this.chimeraRestPort = 0;
379 chimeraListenerThread.requestStop();
380 chimeraListenerThread = null;
382 structureManager.clearOnChimeraExit();
385 public void exitChimera()
387 if (isChimeraLaunched() && chimera != null)
389 sendChimeraCommand("stop really", false);
393 } catch (Exception ex)
398 clearOnChimeraExit();
401 public Map<Integer, ChimeraModel> getSelectedModels()
403 Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
404 List<String> chimeraReply = sendChimeraCommand(
405 "list selection level molecule", true);
406 if (chimeraReply != null)
408 for (String modelLine : chimeraReply)
410 ChimeraModel chimeraModel = new ChimeraModel(modelLine);
411 Integer modelKey = ChimUtils.makeModelKey(
412 chimeraModel.getModelNumber(),
413 chimeraModel.getSubModelNumber());
414 selectedModelsMap.put(modelKey, chimeraModel);
417 return selectedModelsMap;
420 public List<String> getSelectedResidueSpecs()
422 List<String> selectedResidues = new ArrayList<String>();
423 List<String> chimeraReply = sendChimeraCommand(
424 "list selection level residue", true);
425 if (chimeraReply != null)
427 for (String inputLine : chimeraReply)
429 String[] inputLineParts = inputLine.split("\\s+");
430 if (inputLineParts.length == 5)
432 selectedResidues.add(inputLineParts[2]);
436 return selectedResidues;
439 public void getSelectedResidues(
440 Map<Integer, ChimeraModel> selectedModelsMap)
442 List<String> chimeraReply = sendChimeraCommand(
443 "list selection level residue", true);
444 if (chimeraReply != null)
446 for (String inputLine : chimeraReply)
448 ChimeraResidue r = new ChimeraResidue(inputLine);
449 Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
450 r.getSubModelNumber());
451 if (selectedModelsMap.containsKey(modelKey))
453 ChimeraModel model = selectedModelsMap.get(modelKey);
461 * Return the list of ChimeraModels currently open. Warning: if smiles model
462 * name too long, only part of it with "..." is printed.
465 * @return List of ChimeraModel's
467 // TODO: [Optional] Handle smiles names in a better way in Chimera?
468 public List<ChimeraModel> getModelList()
470 List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
471 List<String> list = sendChimeraCommand("list models type molecule",
475 for (String modelLine : list)
477 ChimeraModel chimeraModel = new ChimeraModel(modelLine);
478 modelList.add(chimeraModel);
485 * Return the list of depiction presets available from within Chimera. Chimera
486 * will return the list as a series of lines with the format: Preset type
487 * number "description"
489 * @return list of presets
491 public List<String> getPresets()
493 ArrayList<String> presetList = new ArrayList<String>();
494 List<String> output = sendChimeraCommand("preset list", true);
497 for (String preset : output)
499 preset = preset.substring(7); // Skip over the "Preset"
500 preset = preset.replaceFirst("\"", "(");
501 preset = preset.replaceFirst("\"", ")");
502 // string now looks like: type number (description)
503 presetList.add(preset);
509 public boolean isChimeraLaunched()
511 boolean launched = false;
517 // if we get here, process has ended
518 } catch (IllegalThreadStateException e)
520 // ok - not yet terminated
527 public boolean launchChimera(List<String> chimeraPaths)
529 // Do nothing if Chimera is already launched
530 if (isChimeraLaunched())
535 // Try to launch Chimera (eventually using one of the possible paths)
536 String error = "Error message: ";
537 String workingPath = "";
538 // iterate over possible paths for starting Chimera
539 for (String chimeraPath : chimeraPaths)
541 File path = new File(chimeraPath);
542 if (!path.canExecute())
544 error += "File '" + path + "' does not exist.\n";
549 List<String> args = new ArrayList<String>();
550 args.add(chimeraPath);
552 args.add(USE_REST ? "RESTServer" : "ReadStdin");
553 ProcessBuilder pb = new ProcessBuilder(args);
554 chimera = pb.start();
556 workingPath = chimeraPath;
557 logger.info("Starting " + chimeraPath + " with "
558 + (USE_REST ? "REST API" : "stdin/stdout"));
560 } catch (Exception e)
562 // Chimera could not be started
563 error += e.getMessage();
566 // If no error, then Chimera was launched successfully
567 if (error.length() == 0)
571 this.chimeraRestPort = getPortNumber();
572 System.out.println("Chimera REST API on port " + chimeraRestPort);
576 // Initialize the listener threads
577 chimeraListenerThread = new ListenerThreads(chimera,
579 chimeraListenerThread.start();
581 // structureManager.initChimTable();
582 structureManager.setChimeraPathProperty(workingPath);
583 // TODO: [Optional] Check Chimera version and show a warning if below 1.8
584 // Ask Chimera to give us updates
589 // Tell the user that Chimera could not be started because of an error
595 * Read and return the port number returned in the reply to --start RESTServer
597 private int getPortNumber()
600 InputStream readChan = chimera.getInputStream();
601 BufferedReader lineReader = new BufferedReader(new InputStreamReader(
603 String response = null;
606 // expect: REST server on host 127.0.0.1 port port_number
607 response = lineReader.readLine();
608 String [] tokens = response.split(" ");
609 if (tokens.length == 7 && "port".equals(tokens[5])) {
610 port = Integer.parseInt(tokens[6]);
611 logger.info("Chimera REST service listening on port "
614 } catch (Exception e)
616 logger.error("Failed to get REST port number from " + response + ": "
623 } catch (IOException e2)
631 * Determine the color that Chimera is using for this model.
634 * the ChimeraModel we want to get the Color for
635 * @return the default model Color for this model in Chimera
637 public Color getModelColor(ChimeraModel model)
639 List<String> colorLines = sendChimeraCommand(
640 "list model spec " + model.toSpec() + " attribute color", true);
641 if (colorLines == null || colorLines.size() == 0)
645 return ChimUtils.parseModelColor(colorLines.get(0));
650 * Get information about the residues associated with a model. This uses the
651 * Chimera listr command. We don't return the resulting residues, but we add
652 * the residues to the model.
655 * the ChimeraModel to get residue information for
658 public void addResidues(ChimeraModel model)
660 int modelNumber = model.getModelNumber();
661 int subModelNumber = model.getSubModelNumber();
662 // Get the list -- it will be in the reply log
663 List<String> reply = sendChimeraCommand(
664 "list residues spec " + model.toSpec(), true);
669 for (String inputLine : reply)
671 ChimeraResidue r = new ChimeraResidue(inputLine);
672 if (r.getModelNumber() == modelNumber
673 || r.getSubModelNumber() == subModelNumber)
680 public List<String> getAttrList()
682 List<String> attributes = new ArrayList<String>();
683 final List<String> reply = sendChimeraCommand("list resattr", true);
686 for (String inputLine : reply)
688 String[] lineParts = inputLine.split("\\s");
689 if (lineParts.length == 2 && lineParts[0].equals("resattr"))
691 attributes.add(lineParts[1]);
698 public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
701 Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
702 final List<String> reply = sendChimeraCommand("list residue spec "
703 + model.toSpec() + " attribute " + aCommand, true);
706 for (String inputLine : reply)
708 String[] lineParts = inputLine.split("\\s");
709 if (lineParts.length == 5)
711 ChimeraResidue residue = ChimUtils
712 .getResidue(lineParts[2], model);
713 String value = lineParts[4];
716 if (value.equals("None"))
720 if (value.equals("True") || value.equals("False"))
722 values.put(residue, Boolean.valueOf(value));
727 Double doubleValue = Double.valueOf(value);
728 values.put(residue, doubleValue);
729 } catch (NumberFormatException ex)
731 values.put(residue, value);
740 private volatile boolean busy = false;
743 * Send a command to Chimera.
746 * Command string to be send.
748 * Flag indicating whether the method should return the reply from
750 * @return List of Strings corresponding to the lines in the Chimera reply or
753 public List<String> sendChimeraCommand(String command, boolean reply)
755 if (!isChimeraLaunched() || command == null
756 || "".equals(command.trim()))
760 // TODO do we need a maximum wait time before aborting?
766 } catch (InterruptedException q)
772 long startTime = System.currentTimeMillis();
777 return sendRestCommand(command);
781 return sendStdinCommand(command, reply);
788 System.out.println("Chimera command took "
789 + (System.currentTimeMillis() - startTime) + "ms: "
797 * Sends the command to Chimera's REST API, and returns any response lines.
802 protected List<String> sendRestCommand(String command)
804 // TODO start a separate thread to do this so we don't block?
805 String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
806 List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
807 commands.add(new BasicNameValuePair("command", command));
809 List<String> reply = new ArrayList<String>();
810 BufferedReader response = null;
812 response = HttpClientUtils.doHttpUrlPost(restUrl,
815 while ((line = response.readLine()) != null) {
818 } catch (Exception e)
820 logger.error("REST call " + command + " failed: " + e.getMessage());
823 if (response != null)
828 } catch (IOException e)
837 * Send a command to stdin of Chimera process, and optionally read any
844 protected List<String> sendStdinCommand(String command, boolean readReply)
846 chimeraListenerThread.clearResponse(command);
847 String text = command.concat("\n");
851 chimera.getOutputStream().write(text.getBytes());
852 chimera.getOutputStream().flush();
853 } catch (IOException e)
855 // logger.info("Unable to execute command: " + text);
856 // logger.info("Exiting...");
857 logger.warn("Unable to execute command: " + text);
858 logger.warn("Exiting...");
859 clearOnChimeraExit();
866 List<String> rsp = chimeraListenerThread.getResponse(command);
870 public StructureManager getStructureManager()
872 return structureManager;
875 public boolean isBusy()