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.io.UnsupportedEncodingException;
42 import java.net.URLEncoder;
43 import java.nio.charset.StandardCharsets;
44 import java.nio.file.Paths;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.HashMap;
48 import java.util.List;
51 import org.apache.http.NameValuePair;
52 import org.apache.http.message.BasicNameValuePair;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
56 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
57 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
58 import jalview.ws.HttpClientUtils;
61 * This object maintains the Chimera communication information.
63 public class ChimeraManager
65 private static final int REST_REPLY_TIMEOUT_MS = 15000;
67 private static final int CONNECTION_TIMEOUT_MS = 100;
69 private static final boolean debug = false;
71 private int chimeraRestPort;
73 private Process chimera;
75 private ListenerThreads chimeraListenerThread;
77 private Map<Integer, ChimeraModel> currentModelsMap;
79 private Logger logger = LoggerFactory
80 .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
82 private StructureManager structureManager;
84 public ChimeraManager(StructureManager structureManager)
86 this.structureManager = structureManager;
88 chimeraListenerThread = null;
89 currentModelsMap = new HashMap<>();
93 public List<ChimeraModel> getChimeraModels(String modelName)
95 List<ChimeraModel> models = getChimeraModels(modelName,
97 models.addAll(getChimeraModels(modelName, ModelType.SMILES));
101 public List<ChimeraModel> getChimeraModels(String modelName,
104 List<ChimeraModel> models = new ArrayList<>();
105 for (ChimeraModel model : currentModelsMap.values())
107 if (modelName.equals(model.getModelName())
108 && modelType.equals(model.getModelType()))
116 public Map<String, List<ChimeraModel>> getChimeraModelsMap()
118 Map<String, List<ChimeraModel>> models = new HashMap<>();
119 for (ChimeraModel model : currentModelsMap.values())
121 String modelName = model.getModelName();
122 if (!models.containsKey(modelName))
124 models.put(modelName, new ArrayList<ChimeraModel>());
126 if (!models.get(modelName).contains(model))
128 models.get(modelName).add(model);
134 public ChimeraModel getChimeraModel(Integer modelNumber,
135 Integer subModelNumber)
137 Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
138 if (currentModelsMap.containsKey(key))
140 return currentModelsMap.get(key);
145 public ChimeraModel getChimeraModel()
147 return currentModelsMap.values().iterator().next();
150 public Collection<ChimeraModel> getChimeraModels()
152 // this method is invoked by the model navigator dialog
153 return currentModelsMap.values();
156 public int getChimeraModelsCount(boolean smiles)
158 // this method is invokes by the model navigator dialog
159 int counter = currentModelsMap.size();
165 for (ChimeraModel model : currentModelsMap.values())
167 if (model.getModelType() == ModelType.SMILES)
175 public boolean hasChimeraModel(Integer modelNubmer)
177 return hasChimeraModel(modelNubmer, 0);
180 public boolean hasChimeraModel(Integer modelNubmer,
181 Integer subModelNumber)
183 return currentModelsMap.containsKey(
184 ChimUtils.makeModelKey(modelNubmer, subModelNumber));
187 public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
190 currentModelsMap.put(
191 ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
194 public void removeChimeraModel(Integer modelNumber,
195 Integer subModelNumber)
197 int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
198 if (currentModelsMap.containsKey(modelKey))
200 currentModelsMap.remove(modelKey);
204 public List<ChimeraModel> openModel(String modelPath, ModelType type)
206 return openModel(modelPath, getFileNameFromPath(modelPath), type);
210 * Overloaded method to allow Jalview to pass in a model name.
217 public List<ChimeraModel> openModel(String modelPath, String modelName,
220 logger.info("chimera open " + modelPath);
222 List<ChimeraModel> modelList = getModelList();
223 List<String> response = null;
224 // TODO: [Optional] Handle modbase models
225 if (type == ModelType.MODBASE_MODEL)
227 response = sendChimeraCommand("open modbase:" + modelPath, true);
228 // } else if (type == ModelType.SMILES) {
229 // response = sendChimeraCommand("open smiles:" + modelName, true);
230 // modelName = "smiles:" + modelName;
234 response = sendChimeraCommand("open " + modelPath, true);
236 if (response == null)
238 // something went wrong
239 logger.warn("Could not open " + modelPath);
243 // patch for Jalview - set model name in Chimera
244 // TODO: find a variant that works for sub-models
245 for (ChimeraModel newModel : getModelList())
247 if (!modelList.contains(newModel))
249 newModel.setModelName(modelName);
250 sendChimeraCommand("setattr M name " + modelName + " #"
251 + newModel.getModelNumber(), false);
252 modelList.add(newModel);
256 // assign color and residues to open models
257 for (ChimeraModel chimeraModel : modelList)
260 Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel);
261 if (modelColor != null)
263 chimeraModel.setModelColor(modelColor);
266 // Get our properties (default color scheme, etc.)
267 // Make the molecule look decent
268 // chimeraSend("repr stick "+newModel.toSpec());
270 // Create the information we need for the navigator
271 if (type != ModelType.SMILES && !isChimeraX())
273 addResidues(chimeraModel);
277 sendChimeraCommand("focus", false);
278 // startListening(); // see ChimeraListener
283 * Refactored method to extract the last (or only) element delimited by file
289 private String getFileNameFromPath(String modelPath)
291 String modelName = modelPath;
292 if (modelPath == null)
296 // TODO: [Optional] Convert path to name in a better way
297 if (modelPath.lastIndexOf(File.separator) > 0)
299 modelName = modelPath
300 .substring(modelPath.lastIndexOf(File.separator) + 1);
302 else if (modelPath.lastIndexOf("/") > 0)
304 modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1);
309 public void closeModel(ChimeraModel model)
311 // int model = structure.modelNumber();
312 // int subModel = structure.subModelNumber();
313 // Integer modelKey = makeModelKey(model, subModel);
315 logger.info("chimera close model " + model.getModelName());
316 if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
317 model.getModelNumber(), model.getSubModelNumber())))
319 sendChimeraCommand("close " + model.toSpec(), false);
320 // currentModelNamesMap.remove(model.getModelName());
321 currentModelsMap.remove(ChimUtils.makeModelKey(model.getModelNumber(),
322 model.getSubModelNumber()));
323 // selectionList.remove(chimeraModel);
327 logger.warn("Could not find model " + model.getModelName()
333 public void startListening()
335 sendChimeraCommand("listen start models; listen start selection",
339 public void stopListening()
341 String command = "listen stop models ; listen stop selection ";
342 sendChimeraCommand(command, false);
346 * Tell Chimera we are listening on the given URI
350 public void startListening(String uri)
353 * listen for model changes
355 String command = "listen start models url " + uri;
356 sendChimeraCommand(command, false);
359 * listen for selection changes
361 command = "listen start select prefix SelectionChanged url " + uri;
362 sendChimeraCommand(command, false);
366 * Select something in Chimera
369 * the selection command to pass to Chimera
371 public void select(String command)
373 sendChimeraCommand("listen stop selection; " + command
374 + "; listen start selection", false);
379 sendChimeraCommand("focus", false);
382 public void clearOnChimeraExit()
385 currentModelsMap.clear();
386 this.chimeraRestPort = 0;
387 structureManager.clearOnChimeraExit();
390 public void exitChimera()
392 if (isChimeraLaunched() && chimera != null)
394 sendChimeraCommand("stop really", false);
397 // TODO is this too violent? could it force close the process
398 // before it has done an orderly shutdown?
400 } catch (Exception ex)
405 clearOnChimeraExit();
408 public Map<Integer, ChimeraModel> getSelectedModels()
410 Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
411 List<String> chimeraReply = sendChimeraCommand(
412 "list selection level molecule", true);
413 if (chimeraReply != null)
415 for (String modelLine : chimeraReply)
417 ChimeraModel chimeraModel = new ChimeraModel(modelLine);
418 Integer modelKey = ChimUtils.makeModelKey(
419 chimeraModel.getModelNumber(),
420 chimeraModel.getSubModelNumber());
421 selectedModelsMap.put(modelKey, chimeraModel);
424 return selectedModelsMap;
428 * Sends a 'list selection level residue' command to Chimera and returns the
429 * list of selected atomspecs
433 public List<String> getSelectedResidueSpecs()
435 List<String> selectedResidues = new ArrayList<>();
437 String command = "list selection level residue";
438 List<String> chimeraReply = sendChimeraCommand(command, true);
439 if (chimeraReply != null)
442 * expect 0, 1 or more lines of the format either
444 * residue id #0:43.A type GLY
446 * residue id /A:89 name THR index 88
447 * We are only interested in the atomspec (third token of the reply)
449 for (String inputLine : chimeraReply)
451 String[] inputLineParts = inputLine.split("\\s+");
452 if (inputLineParts.length >= 5)
454 selectedResidues.add(inputLineParts[2]);
458 return selectedResidues;
461 public void getSelectedResidues(
462 Map<Integer, ChimeraModel> selectedModelsMap)
464 List<String> chimeraReply = sendChimeraCommand(
465 "list selection level residue", true);
466 if (chimeraReply != null)
468 for (String inputLine : chimeraReply)
470 ChimeraResidue r = new ChimeraResidue(inputLine);
471 Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
472 r.getSubModelNumber());
473 if (selectedModelsMap.containsKey(modelKey))
475 ChimeraModel model = selectedModelsMap.get(modelKey);
483 * Return the list of ChimeraModels currently open. Warning: if smiles model
484 * name too long, only part of it with "..." is printed.
487 * @return List of ChimeraModel's
489 // TODO: [Optional] Handle smiles names in a better way in Chimera?
490 public List<ChimeraModel> getModelList()
492 List<ChimeraModel> modelList = new ArrayList<>();
493 String command = "list models type "
494 + (isChimeraX() ? "AtomicStructure" : "molecule");
495 List<String> list = sendChimeraCommand(command, true);
498 for (String modelLine : list)
502 ChimeraModel chimeraModel = new ChimeraModel(modelLine);
503 modelList.add(chimeraModel);
504 } catch (NullPointerException e)
514 * Return the list of depiction presets available from within Chimera. Chimera
515 * will return the list as a series of lines with the format: Preset type
516 * number "description"
518 * @return list of presets
520 public List<String> getPresets()
522 ArrayList<String> presetList = new ArrayList<>();
523 List<String> output = sendChimeraCommand("preset list", true);
526 for (String preset : output)
528 preset = preset.substring(7); // Skip over the "Preset"
529 preset = preset.replaceFirst("\"", "(");
530 preset = preset.replaceFirst("\"", ")");
531 // string now looks like: type number (description)
532 presetList.add(preset);
538 public boolean isChimeraLaunched()
540 boolean launched = false;
546 // if we get here, process has ended
547 } catch (IllegalThreadStateException e)
549 // ok - not yet terminated
557 * Launch Chimera, unless an instance linked to this object is already
558 * running. Returns true if chimera is successfully launched, or already
559 * running, else false.
561 * @param chimeraPaths
564 public boolean launchChimera(List<String> chimeraPaths)
566 // Do nothing if Chimera is already launched
567 if (isChimeraLaunched())
572 // Try to launch Chimera (eventually using one of the possible paths)
573 String error = "Error message: ";
574 String workingPath = "";
575 // iterate over possible paths for starting Chimera
576 for (String chimeraPath : chimeraPaths)
580 // ensure symbolic links are resolved
581 chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
582 File path = new File(chimeraPath);
583 // uncomment the next line to simulate Chimera not installed
584 // path = new File(chimeraPath + "x");
585 if (!path.canExecute())
587 error += "File '" + path + "' does not exist.\n";
590 List<String> args = new ArrayList<>();
591 args.add(chimeraPath);
592 // shows Chimera output window but suppresses REST responses:
593 // args.add("--debug");
594 addLaunchArguments(args);
595 ProcessBuilder pb = new ProcessBuilder(args);
596 chimera = pb.start();
598 workingPath = chimeraPath;
600 } catch (Exception e)
602 // Chimera could not be started using this path
603 error += e.getMessage();
606 // If no error, then Chimera was launched successfully
607 if (error.length() == 0)
609 this.chimeraRestPort = getPortNumber();
611 "Chimera REST API started on port " + chimeraRestPort);
612 // structureManager.initChimTable();
613 structureManager.setChimeraPathProperty(workingPath);
614 // TODO: [Optional] Check Chimera version and show a warning if below 1.8
615 // Ask Chimera to give us updates
616 // startListening(); // later - see ChimeraListener
617 return (chimeraRestPort > 0);
620 // Tell the user that Chimera could not be started because of an error
626 * Adds command-line arguments to start the REST server
628 * Method extracted for Jalview to allow override in ChimeraXManager
632 protected void addLaunchArguments(List<String> args)
635 args.add("RESTServer");
639 * Read and return the port number returned in the reply to --start RESTServer
641 private int getPortNumber()
644 InputStream readChan = chimera.getInputStream();
645 BufferedReader lineReader = new BufferedReader(
646 new InputStreamReader(readChan));
647 StringBuilder responses = new StringBuilder();
650 String response = lineReader.readLine();
651 while (response != null)
653 responses.append("\n" + response);
654 // expect: REST server on host 127.0.0.1 port port_number
655 // ChimeraX is the same except "REST server started on host..."
656 if (response.startsWith("REST server"))
658 String[] tokens = response.split(" ");
659 for (int i = 0; i < tokens.length - 1; i++)
661 if ("port".equals(tokens[i]))
663 port = Integer.parseInt(tokens[i + 1]);
670 break; // hack for hanging readLine()
672 response = lineReader.readLine();
674 } catch (Exception e)
676 logger.error("Failed to get REST port number from " + responses + ": "
683 } catch (IOException e2)
690 "Failed to start Chimera with REST service, response was: "
694 "Chimera REST service listening on port " + chimeraRestPort);
699 * Determine the color that Chimera is using for this model.
702 * the ChimeraModel we want to get the Color for
703 * @return the default model Color for this model in Chimera
705 public Color getModelColor(ChimeraModel model)
707 List<String> colorLines = sendChimeraCommand(
708 "list model spec " + model.toSpec() + " attribute color", true);
709 if (colorLines == null || colorLines.size() == 0)
713 return ChimUtils.parseModelColor(colorLines.get(0));
718 * Get information about the residues associated with a model. This uses the
719 * Chimera listr command. We don't return the resulting residues, but we add
720 * the residues to the model.
723 * the ChimeraModel to get residue information for
726 public void addResidues(ChimeraModel model)
728 int modelNumber = model.getModelNumber();
729 int subModelNumber = model.getSubModelNumber();
730 // Get the list -- it will be in the reply log
731 List<String> reply = sendChimeraCommand(
732 "list residues spec " + model.toSpec(), true);
737 for (String inputLine : reply)
739 ChimeraResidue r = new ChimeraResidue(inputLine);
740 if (r.getModelNumber() == modelNumber
741 || r.getSubModelNumber() == subModelNumber)
748 public List<String> getAttrList()
750 List<String> attributes = new ArrayList<>();
751 String command = (isChimeraX() ? "info " : "list ") + "resattr";
752 final List<String> reply = sendChimeraCommand(command, true);
755 for (String inputLine : reply)
757 String[] lineParts = inputLine.split("\\s");
758 if (lineParts.length == 2 && lineParts[0].equals("resattr"))
760 attributes.add(lineParts[1]);
767 public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
770 Map<ChimeraResidue, Object> values = new HashMap<>();
771 final List<String> reply = sendChimeraCommand("list residue spec "
772 + model.toSpec() + " attribute " + aCommand, true);
775 for (String inputLine : reply)
777 String[] lineParts = inputLine.split("\\s");
778 if (lineParts.length == 5)
780 ChimeraResidue residue = ChimUtils.getResidue(lineParts[2],
782 String value = lineParts[4];
785 if (value.equals("None"))
789 if (value.equals("True") || value.equals("False"))
791 values.put(residue, Boolean.valueOf(value));
796 Double doubleValue = Double.valueOf(value);
797 values.put(residue, doubleValue);
798 } catch (NumberFormatException ex)
800 values.put(residue, value);
809 private volatile boolean busy = false;
812 * Send a command to Chimera.
815 * Command string to be send.
817 * Flag indicating whether the method should return the reply from
819 * @return List of Strings corresponding to the lines in the Chimera reply or
822 public List<String> sendChimeraCommand(String command, boolean reply)
826 System.out.println("chimeradebug>> " + command);
828 if (!isChimeraLaunched() || command == null
829 || "".equals(command.trim()))
834 * set a maximum wait time before trying anyway
835 * to avoid hanging indefinitely
839 while (busy && waited < 1001)
845 } catch (InterruptedException q)
850 long startTime = System.currentTimeMillis();
853 return sendRestCommand(command);
857 * Make sure busy flag is reset come what may!
862 System.out.println("Chimera command took "
863 + (System.currentTimeMillis() - startTime) + "ms: "
870 * Sends the command to Chimera's REST API, and returns any response lines.
875 protected List<String> sendRestCommand(String command)
877 String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
878 List<NameValuePair> commands = new ArrayList<>(1);
879 String method = getHttpRequestMethod();
880 if ("GET".equals(method))
884 command = URLEncoder.encode(command, StandardCharsets.UTF_8.name());
885 } catch (UnsupportedEncodingException e)
887 command = command.replace(" ", "+").replace("#", "%23")
888 .replace("|", "%7C").replace(";", "%3B")
889 .replace(":", "%3A");
892 commands.add(new BasicNameValuePair("command", command));
894 List<String> reply = new ArrayList<>();
895 BufferedReader response = null;
898 response = "GET".equals(method)
899 ? HttpClientUtils.doHttpGet(restUrl, commands,
900 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS)
901 : HttpClientUtils.doHttpUrlPost(restUrl, commands,
902 CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
904 while ((line = response.readLine()) != null)
908 } catch (Exception e)
910 logger.error("REST call '" + command + "' failed: " + e.getMessage());
913 if (response != null)
918 } catch (IOException e)
927 * Returns "POST" as the HTTP request method to use for REST service calls to
932 protected String getHttpRequestMethod()
938 * Send a command to stdin of Chimera process, and optionally read any
945 protected List<String> sendStdinCommand(String command, boolean readReply)
947 chimeraListenerThread.clearResponse(command);
948 String text = command.concat("\n");
952 chimera.getOutputStream().write(text.getBytes());
953 chimera.getOutputStream().flush();
954 } catch (IOException e)
956 // logger.info("Unable to execute command: " + text);
957 // logger.info("Exiting...");
958 logger.warn("Unable to execute command: " + text);
959 logger.warn("Exiting...");
960 clearOnChimeraExit();
967 List<String> rsp = chimeraListenerThread.getResponse(command);
971 public StructureManager getStructureManager()
973 return structureManager;
976 public boolean isBusy()
981 public Process getChimeraProcess()
986 public boolean isChimeraX()