3 * Copyright (c) 2006 The Regents of the University of California.
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions, and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions, and the following
13 * disclaimer in the documentation and/or other materials provided
14 * with the distribution.
15 * 3. Redistributions must acknowledge that this software was
16 * originally developed by the UCSF Computer Graphics Laboratory
17 * under support by the NIH National Center for Research Resources,
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
21 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS BE LIABLE
24 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
26 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
30 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 package ext.edu.ucsf.rbvi.strucviz2;
35 import jalview.ws.HttpClientUtils;
37 import java.awt.Color;
38 import java.io.BufferedReader;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.InputStreamReader;
43 import java.nio.file.Paths;
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.HashMap;
47 import java.util.List;
50 import org.apache.http.NameValuePair;
51 import org.apache.http.message.BasicNameValuePair;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
55 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
56 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
59 * This object maintains the Chimera communication information.
61 public class ChimeraManager
63 private static final int REST_REPLY_TIMEOUT_MS = 15000;
65 private static final int CONNECTION_TIMEOUT_MS = 100;
67 private static final boolean debug = false;
69 private int chimeraRestPort;
71 private Process chimera;
73 private ListenerThreads chimeraListenerThread;
75 private Map<Integer, ChimeraModel> currentModelsMap;
77 private Logger logger = LoggerFactory
78 .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
80 private StructureManager structureManager;
82 public ChimeraManager(StructureManager structureManager)
84 this.structureManager = structureManager;
86 chimeraListenerThread = null;
87 currentModelsMap = new HashMap<>();
91 public List<ChimeraModel> getChimeraModels(String modelName)
93 List<ChimeraModel> models = getChimeraModels(modelName,
95 models.addAll(getChimeraModels(modelName, ModelType.SMILES));
99 public List<ChimeraModel> getChimeraModels(String modelName,
102 List<ChimeraModel> models = new ArrayList<>();
103 for (ChimeraModel model : currentModelsMap.values())
105 if (modelName.equals(model.getModelName())
106 && modelType.equals(model.getModelType()))
114 public Map<String, List<ChimeraModel>> getChimeraModelsMap()
116 Map<String, List<ChimeraModel>> models = new HashMap<>();
117 for (ChimeraModel model : currentModelsMap.values())
119 String modelName = model.getModelName();
120 if (!models.containsKey(modelName))
122 models.put(modelName, new ArrayList<ChimeraModel>());
124 if (!models.get(modelName).contains(model))
126 models.get(modelName).add(model);
132 public ChimeraModel getChimeraModel(Integer modelNumber,
133 Integer subModelNumber)
135 Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
136 if (currentModelsMap.containsKey(key))
138 return currentModelsMap.get(key);
143 public ChimeraModel getChimeraModel()
145 return currentModelsMap.values().iterator().next();
148 public Collection<ChimeraModel> getChimeraModels()
150 // this method is invoked by the model navigator dialog
151 return currentModelsMap.values();
154 public int getChimeraModelsCount(boolean smiles)
156 // this method is invokes by the model navigator dialog
157 int counter = currentModelsMap.size();
163 for (ChimeraModel model : currentModelsMap.values())
165 if (model.getModelType() == ModelType.SMILES)
173 public boolean hasChimeraModel(Integer modelNubmer)
175 return hasChimeraModel(modelNubmer, 0);
178 public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
180 return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
184 public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
187 currentModelsMap.put(
188 ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
191 public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
193 int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
194 if (currentModelsMap.containsKey(modelKey))
196 currentModelsMap.remove(modelKey);
200 public List<ChimeraModel> openModel(String modelPath, ModelType type)
202 return openModel(modelPath, getFileNameFromPath(modelPath), type);
206 * Overloaded method to allow Jalview to pass in a model name.
213 public List<ChimeraModel> openModel(String modelPath, String modelName,
216 logger.info("chimera open " + modelPath);
218 List<ChimeraModel> modelList = getModelList();
219 List<String> response = null;
220 // TODO: [Optional] Handle modbase models
221 if (type == ModelType.MODBASE_MODEL)
223 response = sendChimeraCommand("open modbase:" + modelPath, true);
224 // } else if (type == ModelType.SMILES) {
225 // response = sendChimeraCommand("open smiles:" + modelName, true);
226 // modelName = "smiles:" + modelName;
230 response = sendChimeraCommand("open " + modelPath, true);
232 if (response == null)
234 // something went wrong
235 logger.warn("Could not open " + modelPath);
239 // patch for Jalview - set model name in Chimera
240 // TODO: find a variant that works for sub-models
241 for (ChimeraModel newModel : getModelList())
243 if (!modelList.contains(newModel))
245 newModel.setModelName(modelName);
247 "setattr M name " + modelName + " #"
248 + newModel.getModelNumber(), false);
249 modelList.add(newModel);
253 // assign color and residues to open models
254 for (ChimeraModel chimeraModel : modelList)
257 Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel);
258 if (modelColor != null)
260 chimeraModel.setModelColor(modelColor);
263 // Get our properties (default color scheme, etc.)
264 // Make the molecule look decent
265 // chimeraSend("repr stick "+newModel.toSpec());
267 // Create the information we need for the navigator
268 if (type != ModelType.SMILES && !isChimeraX())
270 addResidues(chimeraModel);
274 sendChimeraCommand("focus", false);
275 // startListening(); // see ChimeraListener
280 * Refactored method to extract the last (or only) element delimited by file
286 private String getFileNameFromPath(String modelPath)
288 String modelName = modelPath;
289 if (modelPath == null)
293 // TODO: [Optional] Convert path to name in a better way
294 if (modelPath.lastIndexOf(File.separator) > 0)
296 modelName = modelPath
297 .substring(modelPath.lastIndexOf(File.separator) + 1);
299 else if (modelPath.lastIndexOf("/") > 0)
301 modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1);
306 public void closeModel(ChimeraModel model)
308 // int model = structure.modelNumber();
309 // int subModel = structure.subModelNumber();
310 // Integer modelKey = makeModelKey(model, subModel);
312 logger.info("chimera close model " + model.getModelName());
313 if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
314 model.getModelNumber(), model.getSubModelNumber())))
316 sendChimeraCommand("close " + model.toSpec(), false);
317 // currentModelNamesMap.remove(model.getModelName());
318 currentModelsMap.remove(ChimUtils.makeModelKey(
319 model.getModelNumber(), model.getSubModelNumber()));
320 // selectionList.remove(chimeraModel);
324 logger.warn("Could not find model " + model.getModelName()
330 public void startListening()
332 sendChimeraCommand("listen start models; listen start selection", false);
335 public void stopListening()
337 sendChimeraCommand("listen stop models ; listen stop selection ", false);
341 * Tell Chimera we are listening on the given URI
345 public void startListening(String uri)
347 sendChimeraCommand("listen start models url " + uri
348 + ";listen start select prefix SelectionChanged url " + uri,
353 * Select something in Chimera
356 * the selection command to pass to Chimera
358 public void select(String command)
360 sendChimeraCommand("listen stop selection; " + command
361 + "; listen start selection", false);
366 sendChimeraCommand("focus", false);
369 public void clearOnChimeraExit()
372 currentModelsMap.clear();
373 this.chimeraRestPort = 0;
374 structureManager.clearOnChimeraExit();
377 public void exitChimera()
379 if (isChimeraLaunched() && chimera != null)
381 sendChimeraCommand("stop really", false);
384 // TODO is this too violent? could it force close the process
385 // before it has done an orderly shutdown?
387 } catch (Exception ex)
392 clearOnChimeraExit();
395 public Map<Integer, ChimeraModel> getSelectedModels()
397 Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
398 List<String> chimeraReply = sendChimeraCommand(
399 "list selection level molecule", true);
400 if (chimeraReply != null)
402 for (String modelLine : chimeraReply)
404 ChimeraModel chimeraModel = new ChimeraModel(modelLine);
405 Integer modelKey = ChimUtils.makeModelKey(
406 chimeraModel.getModelNumber(),
407 chimeraModel.getSubModelNumber());
408 selectedModelsMap.put(modelKey, chimeraModel);
411 return selectedModelsMap;
415 * Sends a 'list selection level residue' command to Chimera and returns the
416 * list of selected atomspecs
420 public List<String> getSelectedResidueSpecs()
422 List<String> selectedResidues = new ArrayList<>();
423 List<String> chimeraReply = sendChimeraCommand(
424 "list selection level residue", true);
425 if (chimeraReply != null)
428 * expect 0, 1 or more lines of the format
429 * residue id #0:43.A type GLY
430 * where we are only interested in the atomspec #0.43.A
432 for (String inputLine : chimeraReply)
434 String[] inputLineParts = inputLine.split("\\s+");
435 if (inputLineParts.length == 5)
437 selectedResidues.add(inputLineParts[2]);
441 return selectedResidues;
444 public void getSelectedResidues(
445 Map<Integer, ChimeraModel> selectedModelsMap)
447 List<String> chimeraReply = sendChimeraCommand(
448 "list selection level residue", true);
449 if (chimeraReply != null)
451 for (String inputLine : chimeraReply)
453 ChimeraResidue r = new ChimeraResidue(inputLine);
454 Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
455 r.getSubModelNumber());
456 if (selectedModelsMap.containsKey(modelKey))
458 ChimeraModel model = selectedModelsMap.get(modelKey);
466 * Return the list of ChimeraModels currently open. Warning: if smiles model
467 * name too long, only part of it with "..." is printed.
470 * @return List of ChimeraModel's
472 // TODO: [Optional] Handle smiles names in a better way in Chimera?
473 public List<ChimeraModel> getModelList()
475 List<ChimeraModel> modelList = new ArrayList<>();
476 modelList.add(new ChimeraModel("4zhp", ModelType.PDB_MODEL, 1, 0));
477 return modelList; // ChimeraX doesn't have 'list models' command
478 // List<String> list = sendChimeraCommand("list models type molecule",
482 // for (String modelLine : list)
484 // ChimeraModel chimeraModel = new ChimeraModel(modelLine);
485 // modelList.add(chimeraModel);
492 * Return the list of depiction presets available from within Chimera. Chimera
493 * will return the list as a series of lines with the format: Preset type
494 * number "description"
496 * @return list of presets
498 public List<String> getPresets()
500 ArrayList<String> presetList = new ArrayList<>();
501 List<String> output = sendChimeraCommand("preset list", true);
504 for (String preset : output)
506 preset = preset.substring(7); // Skip over the "Preset"
507 preset = preset.replaceFirst("\"", "(");
508 preset = preset.replaceFirst("\"", ")");
509 // string now looks like: type number (description)
510 presetList.add(preset);
516 public boolean isChimeraLaunched()
518 boolean launched = false;
524 // if we get here, process has ended
525 } catch (IllegalThreadStateException e)
527 // ok - not yet terminated
535 * Launch Chimera, unless an instance linked to this object is already
536 * running. Returns true if chimera is successfully launched, or already
537 * running, else false.
539 * @param chimeraPaths
542 public boolean launchChimera(List<String> chimeraPaths)
544 // Do nothing if Chimera is already launched
545 if (isChimeraLaunched())
550 // Try to launch Chimera (eventually using one of the possible paths)
551 String error = "Error message: ";
552 String workingPath = "";
553 // iterate over possible paths for starting Chimera
554 for (String chimeraPath : chimeraPaths)
558 // ensure symbolic links are resolved
559 chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
560 isChimeraX = chimeraPath.toLowerCase().contains("chimerax");
561 File path = new File(chimeraPath);
562 // uncomment the next line to simulate Chimera not installed
563 // path = new File(chimeraPath + "x");
564 if (!path.canExecute())
566 error += "File '" + path + "' does not exist.\n";
569 List<String> args = new ArrayList<>();
570 args.add(chimeraPath);
571 // shows Chimera output window but suppresses REST responses:
572 // args.add("--debug");
576 args.add("remote rest start");
581 args.add("RESTServer");
583 ProcessBuilder pb = new ProcessBuilder(args);
584 chimera = pb.start();
586 workingPath = chimeraPath;
588 } catch (Exception e)
590 // Chimera could not be started using this path
591 error += e.getMessage();
594 // If no error, then Chimera was launched successfully
595 if (error.length() == 0)
597 this.chimeraRestPort = getPortNumber();
598 System.out.println("Chimera REST API started on port "
600 // structureManager.initChimTable();
601 structureManager.setChimeraPathProperty(workingPath);
602 // TODO: [Optional] Check Chimera version and show a warning if below 1.8
603 // Ask Chimera to give us updates
604 // startListening(); // later - see ChimeraListener
605 return (chimeraRestPort > 0);
608 // Tell the user that Chimera could not be started because of an error
614 * Read and return the port number returned in the reply to --start RESTServer
616 private int getPortNumber()
619 InputStream readChan = chimera.getInputStream();
620 BufferedReader lineReader = new BufferedReader(new InputStreamReader(
622 StringBuilder responses = new StringBuilder();
625 String response = lineReader.readLine();
626 while (response != null)
628 responses.append("\n" + response);
629 // expect: REST server on host 127.0.0.1 port port_number
630 // ChimeraX is the same except "REST server started on host..."
631 if (response.startsWith("REST server"))
633 String[] tokens = response.split(" ");
634 for (int i = 0; i < tokens.length - 1; i++)
636 if ("port".equals(tokens[i]))
638 port = Integer.parseInt(tokens[i + 1]);
645 break; // hack for hanging readLine()
647 response = lineReader.readLine();
649 } catch (Exception e)
651 logger.error("Failed to get REST port number from " + responses
652 + ": " + e.getMessage());
658 } catch (IOException e2)
665 .println("Failed to start Chimera with REST service, response was: "
668 logger.info("Chimera REST service listening on port " + chimeraRestPort);
673 * Determine the color that Chimera is using for this model.
676 * the ChimeraModel we want to get the Color for
677 * @return the default model Color for this model in Chimera
679 public Color getModelColor(ChimeraModel model)
681 List<String> colorLines = sendChimeraCommand(
682 "list model spec " + model.toSpec() + " attribute color", true);
683 if (colorLines == null || colorLines.size() == 0)
687 return ChimUtils.parseModelColor(colorLines.get(0));
692 * Get information about the residues associated with a model. This uses the
693 * Chimera listr command. We don't return the resulting residues, but we add
694 * the residues to the model.
697 * the ChimeraModel to get residue information for
700 public void addResidues(ChimeraModel model)
702 int modelNumber = model.getModelNumber();
703 int subModelNumber = model.getSubModelNumber();
704 // Get the list -- it will be in the reply log
705 List<String> reply = sendChimeraCommand(
706 "list residues spec " + model.toSpec(), true);
711 for (String inputLine : reply)
713 ChimeraResidue r = new ChimeraResidue(inputLine);
714 if (r.getModelNumber() == modelNumber
715 || r.getSubModelNumber() == subModelNumber)
722 public List<String> getAttrList()
724 List<String> attributes = new ArrayList<>();
725 final List<String> reply = sendChimeraCommand("list resattr", true);
728 for (String inputLine : reply)
730 String[] lineParts = inputLine.split("\\s");
731 if (lineParts.length == 2 && lineParts[0].equals("resattr"))
733 attributes.add(lineParts[1]);
740 public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
743 Map<ChimeraResidue, Object> values = new HashMap<>();
744 final List<String> reply = sendChimeraCommand("list residue spec "
745 + model.toSpec() + " attribute " + aCommand, true);
748 for (String inputLine : reply)
750 String[] lineParts = inputLine.split("\\s");
751 if (lineParts.length == 5)
753 ChimeraResidue residue = ChimUtils
754 .getResidue(lineParts[2], model);
755 String value = lineParts[4];
758 if (value.equals("None"))
762 if (value.equals("True") || value.equals("False"))
764 values.put(residue, Boolean.valueOf(value));
769 Double doubleValue = Double.valueOf(value);
770 values.put(residue, doubleValue);
771 } catch (NumberFormatException ex)
773 values.put(residue, value);
782 private volatile boolean busy = false;
784 private boolean isChimeraX;
787 * Send a command to Chimera.
790 * Command string to be send.
792 * Flag indicating whether the method should return the reply from
794 * @return List of Strings corresponding to the lines in the Chimera reply or
797 public List<String> sendChimeraCommand(String command, boolean reply)
799 System.out.println("chimeradebug>> " + command);
800 if (!isChimeraLaunched() || command == null
801 || "".equals(command.trim()))
805 // TODO do we need a maximum wait time before aborting?
811 } catch (InterruptedException q)
816 long startTime = System.currentTimeMillis();
819 return sendRestCommand(command);
823 * Make sure busy flag is reset come what may!
828 System.out.println("Chimera command took "
829 + (System.currentTimeMillis() - startTime) + "ms: "
837 * Sends the command to Chimera's REST API, and returns any response lines.
842 protected List<String> sendRestCommand(String command)
844 String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
845 List<NameValuePair> commands = new ArrayList<>(1);
846 String encoded = command.replace(" ", "+").replace("#", "%23")
847 .replace("|", "%7C").replace(";", "%3B");
848 commands.add(new BasicNameValuePair("command", encoded));
850 List<String> reply = new ArrayList<>();
851 BufferedReader response = null;
856 response = HttpClientUtils.doHttpGet(restUrl, commands,
857 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
861 response = HttpClientUtils.doHttpUrlPost(restUrl, commands,
862 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
865 while ((line = response.readLine()) != null)
869 } catch (Exception e)
871 logger.error("REST call '" + command + "' failed: " + e.getMessage());
874 if (response != null)
879 } catch (IOException e)
888 * Send a command to stdin of Chimera process, and optionally read any
895 protected List<String> sendStdinCommand(String command, boolean readReply)
897 chimeraListenerThread.clearResponse(command);
898 String text = command.concat("\n");
902 chimera.getOutputStream().write(text.getBytes());
903 chimera.getOutputStream().flush();
904 } catch (IOException e)
906 // logger.info("Unable to execute command: " + text);
907 // logger.info("Exiting...");
908 logger.warn("Unable to execute command: " + text);
909 logger.warn("Exiting...");
910 clearOnChimeraExit();
917 List<String> rsp = chimeraListenerThread.getResponse(command);
921 public StructureManager getStructureManager()
923 return structureManager;
926 public boolean isBusy()
931 public Process getChimeraProcess()
936 public boolean isChimeraX()