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 java.awt.Color;
36 import java.io.BufferedReader;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.io.InputStreamReader;
41 import java.nio.file.Paths;
42 import java.util.ArrayList;
43 import java.util.Collection;
44 import java.util.HashMap;
45 import java.util.List;
48 import org.apache.http.NameValuePair;
49 import org.apache.http.message.BasicNameValuePair;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
53 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
54 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
55 import jalview.ws.HttpClientUtils;
58 * This object maintains the Chimera communication information.
60 public class ChimeraManager
62 private static final int REST_REPLY_TIMEOUT_MS = 15000;
64 private static final int CONNECTION_TIMEOUT_MS = 100;
66 private static final boolean debug = false;
68 private int chimeraRestPort;
70 private Process chimera;
72 private ListenerThreads chimeraListenerThread;
74 private Map<Integer, ChimeraModel> currentModelsMap;
76 private Logger logger = LoggerFactory
77 .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
79 private StructureManager structureManager;
81 public ChimeraManager(StructureManager structureManager)
83 this.structureManager = structureManager;
85 chimeraListenerThread = null;
86 currentModelsMap = new HashMap<>();
90 public List<ChimeraModel> getChimeraModels(String modelName)
92 List<ChimeraModel> models = getChimeraModels(modelName,
94 models.addAll(getChimeraModels(modelName, ModelType.SMILES));
98 public List<ChimeraModel> getChimeraModels(String modelName,
101 List<ChimeraModel> models = new ArrayList<>();
102 for (ChimeraModel model : currentModelsMap.values())
104 if (modelName.equals(model.getModelName())
105 && modelType.equals(model.getModelType()))
113 public Map<String, List<ChimeraModel>> getChimeraModelsMap()
115 Map<String, List<ChimeraModel>> models = new HashMap<>();
116 for (ChimeraModel model : currentModelsMap.values())
118 String modelName = model.getModelName();
119 if (!models.containsKey(modelName))
121 models.put(modelName, new ArrayList<ChimeraModel>());
123 if (!models.get(modelName).contains(model))
125 models.get(modelName).add(model);
131 public ChimeraModel getChimeraModel(Integer modelNumber,
132 Integer subModelNumber)
134 Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
135 if (currentModelsMap.containsKey(key))
137 return currentModelsMap.get(key);
142 public ChimeraModel getChimeraModel()
144 return currentModelsMap.values().iterator().next();
147 public Collection<ChimeraModel> getChimeraModels()
149 // this method is invoked by the model navigator dialog
150 return currentModelsMap.values();
153 public int getChimeraModelsCount(boolean smiles)
155 // this method is invokes by the model navigator dialog
156 int counter = currentModelsMap.size();
162 for (ChimeraModel model : currentModelsMap.values())
164 if (model.getModelType() == ModelType.SMILES)
172 public boolean hasChimeraModel(Integer modelNubmer)
174 return hasChimeraModel(modelNubmer, 0);
177 public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
179 return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
183 public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
186 currentModelsMap.put(
187 ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
190 public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
192 int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
193 if (currentModelsMap.containsKey(modelKey))
195 currentModelsMap.remove(modelKey);
199 public List<ChimeraModel> openModel(String modelPath, ModelType type)
201 return openModel(modelPath, getFileNameFromPath(modelPath), type);
205 * Overloaded method to allow Jalview to pass in a model name.
212 public List<ChimeraModel> openModel(String modelPath, String modelName,
215 logger.info("chimera open " + modelPath);
217 List<ChimeraModel> modelList = getModelList();
218 List<String> response = null;
219 // TODO: [Optional] Handle modbase models
220 if (type == ModelType.MODBASE_MODEL)
222 response = sendChimeraCommand("open modbase:" + modelPath, true);
223 // } else if (type == ModelType.SMILES) {
224 // response = sendChimeraCommand("open smiles:" + modelName, true);
225 // modelName = "smiles:" + modelName;
229 response = sendChimeraCommand("open " + modelPath, true);
231 if (response == null)
233 // something went wrong
234 logger.warn("Could not open " + modelPath);
238 // patch for Jalview - set model name in Chimera
239 // TODO: find a variant that works for sub-models
240 for (ChimeraModel newModel : getModelList())
242 if (!modelList.contains(newModel))
244 newModel.setModelName(modelName);
246 "setattr M name " + modelName + " #"
247 + newModel.getModelNumber(), false);
248 modelList.add(newModel);
252 // assign color and residues to open models
253 for (ChimeraModel chimeraModel : modelList)
256 Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel);
257 if (modelColor != null)
259 chimeraModel.setModelColor(modelColor);
262 // Get our properties (default color scheme, etc.)
263 // Make the molecule look decent
264 // chimeraSend("repr stick "+newModel.toSpec());
266 // Create the information we need for the navigator
267 if (type != ModelType.SMILES && !isChimeraX())
269 addResidues(chimeraModel);
273 sendChimeraCommand("focus", false);
274 // startListening(); // see ChimeraListener
279 * Refactored method to extract the last (or only) element delimited by file
285 private String getFileNameFromPath(String modelPath)
287 String modelName = modelPath;
288 if (modelPath == null)
292 // TODO: [Optional] Convert path to name in a better way
293 if (modelPath.lastIndexOf(File.separator) > 0)
295 modelName = modelPath
296 .substring(modelPath.lastIndexOf(File.separator) + 1);
298 else if (modelPath.lastIndexOf("/") > 0)
300 modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1);
305 public void closeModel(ChimeraModel model)
307 // int model = structure.modelNumber();
308 // int subModel = structure.subModelNumber();
309 // Integer modelKey = makeModelKey(model, subModel);
311 logger.info("chimera close model " + model.getModelName());
312 if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
313 model.getModelNumber(), model.getSubModelNumber())))
315 sendChimeraCommand("close " + model.toSpec(), false);
316 // currentModelNamesMap.remove(model.getModelName());
317 currentModelsMap.remove(ChimUtils.makeModelKey(
318 model.getModelNumber(), model.getSubModelNumber()));
319 // selectionList.remove(chimeraModel);
323 logger.warn("Could not find model " + model.getModelName()
329 public void startListening()
331 sendChimeraCommand("listen start models; listen start selection", false);
334 public void stopListening()
336 String command = "listen stop models ; listen stop selection ";
337 sendChimeraCommand(command, false);
341 * Tell Chimera we are listening on the given URI
345 public void startListening(String uri)
348 * listen for model changes
350 String command = "listen start models url " + uri;
351 sendChimeraCommand(command, false);
354 * listen for selection changes
356 command = "listen start select prefix SelectionChanged url " + uri;
357 sendChimeraCommand(command, false);
361 * Select something in Chimera
364 * the selection command to pass to Chimera
366 public void select(String command)
368 sendChimeraCommand("listen stop selection; " + command
369 + "; listen start selection", false);
374 sendChimeraCommand("focus", false);
377 public void clearOnChimeraExit()
380 currentModelsMap.clear();
381 this.chimeraRestPort = 0;
382 structureManager.clearOnChimeraExit();
385 public void exitChimera()
387 if (isChimeraLaunched() && chimera != null)
389 sendChimeraCommand("stop really", false);
392 // TODO is this too violent? could it force close the process
393 // before it has done an orderly shutdown?
395 } catch (Exception ex)
400 clearOnChimeraExit();
403 public Map<Integer, ChimeraModel> getSelectedModels()
405 Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
406 List<String> chimeraReply = sendChimeraCommand(
407 "list selection level molecule", true);
408 if (chimeraReply != null)
410 for (String modelLine : chimeraReply)
412 ChimeraModel chimeraModel = new ChimeraModel(modelLine);
413 Integer modelKey = ChimUtils.makeModelKey(
414 chimeraModel.getModelNumber(),
415 chimeraModel.getSubModelNumber());
416 selectedModelsMap.put(modelKey, chimeraModel);
419 return selectedModelsMap;
423 * Sends a 'list selection level residue' command to Chimera and returns the
424 * list of selected atomspecs
428 public List<String> getSelectedResidueSpecs()
430 List<String> selectedResidues = new ArrayList<>();
432 String command = "list selection level residue";
433 List<String> chimeraReply = sendChimeraCommand(command, true);
434 if (chimeraReply != null)
437 * expect 0, 1 or more lines of the format either
439 * residue id #0:43.A type GLY
441 * residue id /A:89 name THR index 88
442 * We are only interested in the atomspec (third token of the reply)
444 for (String inputLine : chimeraReply)
446 String[] inputLineParts = inputLine.split("\\s+");
447 if (inputLineParts.length >= 5)
449 selectedResidues.add(inputLineParts[2]);
453 return selectedResidues;
456 public void getSelectedResidues(
457 Map<Integer, ChimeraModel> selectedModelsMap)
459 List<String> chimeraReply = sendChimeraCommand(
460 "list selection level residue", true);
461 if (chimeraReply != null)
463 for (String inputLine : chimeraReply)
465 ChimeraResidue r = new ChimeraResidue(inputLine);
466 Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
467 r.getSubModelNumber());
468 if (selectedModelsMap.containsKey(modelKey))
470 ChimeraModel model = selectedModelsMap.get(modelKey);
478 * Return the list of ChimeraModels currently open. Warning: if smiles model
479 * name too long, only part of it with "..." is printed.
482 * @return List of ChimeraModel's
484 // TODO: [Optional] Handle smiles names in a better way in Chimera?
485 public List<ChimeraModel> getModelList()
487 List<ChimeraModel> modelList = new ArrayList<>();
488 String command = "list models type "
489 + (isChimeraX() ? "AtomicStructure" : "molecule");
490 List<String> list = sendChimeraCommand(command, true);
493 for (String modelLine : list)
497 ChimeraModel chimeraModel = new ChimeraModel(modelLine);
498 modelList.add(chimeraModel);
499 } catch (NullPointerException e)
509 * Return the list of depiction presets available from within Chimera. Chimera
510 * will return the list as a series of lines with the format: Preset type
511 * number "description"
513 * @return list of presets
515 public List<String> getPresets()
517 ArrayList<String> presetList = new ArrayList<>();
518 List<String> output = sendChimeraCommand("preset list", true);
521 for (String preset : output)
523 preset = preset.substring(7); // Skip over the "Preset"
524 preset = preset.replaceFirst("\"", "(");
525 preset = preset.replaceFirst("\"", ")");
526 // string now looks like: type number (description)
527 presetList.add(preset);
533 public boolean isChimeraLaunched()
535 boolean launched = false;
541 // if we get here, process has ended
542 } catch (IllegalThreadStateException e)
544 // ok - not yet terminated
552 * Launch Chimera, unless an instance linked to this object is already
553 * running. Returns true if chimera is successfully launched, or already
554 * running, else false.
556 * @param chimeraPaths
559 public boolean launchChimera(List<String> chimeraPaths)
561 // Do nothing if Chimera is already launched
562 if (isChimeraLaunched())
567 // Try to launch Chimera (eventually using one of the possible paths)
568 String error = "Error message: ";
569 String workingPath = "";
570 // iterate over possible paths for starting Chimera
571 for (String chimeraPath : chimeraPaths)
575 // ensure symbolic links are resolved
576 chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
577 File path = new File(chimeraPath);
578 // uncomment the next line to simulate Chimera not installed
579 // path = new File(chimeraPath + "x");
580 if (!path.canExecute())
582 error += "File '" + path + "' does not exist.\n";
585 List<String> args = new ArrayList<>();
586 args.add(chimeraPath);
587 // shows Chimera output window but suppresses REST responses:
588 // args.add("--debug");
589 addLaunchArguments(args);
590 ProcessBuilder pb = new ProcessBuilder(args);
591 chimera = pb.start();
593 workingPath = chimeraPath;
595 } catch (Exception e)
597 // Chimera could not be started using this path
598 error += e.getMessage();
601 // If no error, then Chimera was launched successfully
602 if (error.length() == 0)
604 this.chimeraRestPort = getPortNumber();
605 System.out.println("Chimera REST API started on port "
607 // structureManager.initChimTable();
608 structureManager.setChimeraPathProperty(workingPath);
609 // TODO: [Optional] Check Chimera version and show a warning if below 1.8
610 // Ask Chimera to give us updates
611 // startListening(); // later - see ChimeraListener
612 return (chimeraRestPort > 0);
615 // Tell the user that Chimera could not be started because of an error
621 * Adds command-line arguments to start the REST server
623 * Method extracted for Jalview to allow override in ChimeraXManager
626 protected void addLaunchArguments(List<String> args)
629 args.add("RESTServer");
633 * Read and return the port number returned in the reply to --start RESTServer
635 private int getPortNumber()
638 InputStream readChan = chimera.getInputStream();
639 BufferedReader lineReader = new BufferedReader(new InputStreamReader(
641 StringBuilder responses = new StringBuilder();
644 String response = lineReader.readLine();
645 while (response != null)
647 responses.append("\n" + response);
648 // expect: REST server on host 127.0.0.1 port port_number
649 // ChimeraX is the same except "REST server started on host..."
650 if (response.startsWith("REST server"))
652 String[] tokens = response.split(" ");
653 for (int i = 0; i < tokens.length - 1; i++)
655 if ("port".equals(tokens[i]))
657 port = Integer.parseInt(tokens[i + 1]);
664 break; // hack for hanging readLine()
666 response = lineReader.readLine();
668 } catch (Exception e)
670 logger.error("Failed to get REST port number from " + responses
671 + ": " + e.getMessage());
677 } catch (IOException e2)
684 .println("Failed to start Chimera with REST service, response was: "
687 logger.info("Chimera REST service listening on port " + chimeraRestPort);
692 * Determine the color that Chimera is using for this model.
695 * the ChimeraModel we want to get the Color for
696 * @return the default model Color for this model in Chimera
698 public Color getModelColor(ChimeraModel model)
700 List<String> colorLines = sendChimeraCommand(
701 "list model spec " + model.toSpec() + " attribute color", true);
702 if (colorLines == null || colorLines.size() == 0)
706 return ChimUtils.parseModelColor(colorLines.get(0));
711 * Get information about the residues associated with a model. This uses the
712 * Chimera listr command. We don't return the resulting residues, but we add
713 * the residues to the model.
716 * the ChimeraModel to get residue information for
719 public void addResidues(ChimeraModel model)
721 int modelNumber = model.getModelNumber();
722 int subModelNumber = model.getSubModelNumber();
723 // Get the list -- it will be in the reply log
724 List<String> reply = sendChimeraCommand(
725 "list residues spec " + model.toSpec(), true);
730 for (String inputLine : reply)
732 ChimeraResidue r = new ChimeraResidue(inputLine);
733 if (r.getModelNumber() == modelNumber
734 || r.getSubModelNumber() == subModelNumber)
741 public List<String> getAttrList()
743 List<String> attributes = new ArrayList<>();
744 String command = (isChimeraX() ? "info " : "list ") + "resattr";
745 final List<String> reply = sendChimeraCommand(command, true);
748 for (String inputLine : reply)
750 String[] lineParts = inputLine.split("\\s");
751 if (lineParts.length == 2 && lineParts[0].equals("resattr"))
753 attributes.add(lineParts[1]);
760 public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
763 Map<ChimeraResidue, Object> values = new HashMap<>();
764 final List<String> reply = sendChimeraCommand("list residue spec "
765 + model.toSpec() + " attribute " + aCommand, true);
768 for (String inputLine : reply)
770 String[] lineParts = inputLine.split("\\s");
771 if (lineParts.length == 5)
773 ChimeraResidue residue = ChimUtils
774 .getResidue(lineParts[2], model);
775 String value = lineParts[4];
778 if (value.equals("None"))
782 if (value.equals("True") || value.equals("False"))
784 values.put(residue, Boolean.valueOf(value));
789 Double doubleValue = Double.valueOf(value);
790 values.put(residue, doubleValue);
791 } catch (NumberFormatException ex)
793 values.put(residue, value);
802 private volatile boolean busy = false;
805 * Send a command to Chimera.
808 * Command string to be send.
810 * Flag indicating whether the method should return the reply from
812 * @return List of Strings corresponding to the lines in the Chimera reply or
815 public List<String> sendChimeraCommand(String command, boolean reply)
817 System.out.println("chimeradebug>> " + command);
818 if (!isChimeraLaunched() || command == null
819 || "".equals(command.trim()))
823 // TODO do we need a maximum wait time before aborting?
829 } catch (InterruptedException q)
834 long startTime = System.currentTimeMillis();
837 return sendRestCommand(command);
841 * Make sure busy flag is reset come what may!
846 System.out.println("Chimera command took "
847 + (System.currentTimeMillis() - startTime) + "ms: "
855 * Sends the command to Chimera's REST API, and returns any response lines.
860 protected List<String> sendRestCommand(String command)
862 String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
863 List<NameValuePair> commands = new ArrayList<>(1);
864 String method = getHttpRequestMethod();
865 if ("GET".equals(method))
867 command = command.replace(" ", "+").replace("#", "%23")
868 .replace("|", "%7C").replace(";", "%3B");
870 commands.add(new BasicNameValuePair("command", command));
872 List<String> reply = new ArrayList<>();
873 BufferedReader response = null;
876 response = "GET".equals(method)
877 ? HttpClientUtils.doHttpGet(restUrl, commands,
878 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS)
879 : HttpClientUtils.doHttpUrlPost(restUrl, commands,
880 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
882 while ((line = response.readLine()) != null)
886 } catch (Exception e)
888 logger.error("REST call '" + command + "' failed: " + e.getMessage());
891 if (response != null)
896 } catch (IOException e)
905 * Returns "POST" as the HTTP request method to use for REST service calls to Chimera
908 protected String getHttpRequestMethod()
914 * Send a command to stdin of Chimera process, and optionally read any
921 protected List<String> sendStdinCommand(String command, boolean readReply)
923 chimeraListenerThread.clearResponse(command);
924 String text = command.concat("\n");
928 chimera.getOutputStream().write(text.getBytes());
929 chimera.getOutputStream().flush();
930 } catch (IOException e)
932 // logger.info("Unable to execute command: " + text);
933 // logger.info("Exiting...");
934 logger.warn("Unable to execute command: " + text);
935 logger.warn("Exiting...");
936 clearOnChimeraExit();
943 List<String> rsp = chimeraListenerThread.getResponse(command);
947 public StructureManager getStructureManager()
949 return structureManager;
952 public boolean isBusy()
957 public Process getChimeraProcess()
962 public boolean isChimeraX()