JAL-2035 improvement to bind Jalview to Chimera model when opening an mmCIF file
[jalview.git] / src / ext / edu / ucsf / rbvi / strucviz2 / ChimeraManager.java
1 package ext.edu.ucsf.rbvi.strucviz2;
2
3 import jalview.ws.HttpClientUtils;
4
5 import java.awt.Color;
6 import java.io.BufferedReader;
7 import java.io.File;
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;
15 import java.util.Map;
16
17 import org.apache.http.NameValuePair;
18 import org.apache.http.message.BasicNameValuePair;
19 import org.slf4j.Logger;
20 import org.slf4j.LoggerFactory;
21
22 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
23 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
24
25 /**
26  * This object maintains the Chimera communication information.
27  */
28 public class ChimeraManager
29 {
30   private static final boolean debug = false;
31
32   private int chimeraRestPort;
33
34   private Process chimera;
35
36   private ListenerThreads chimeraListenerThread;
37
38   private Map<Integer, ChimeraModel> currentModelsMap;
39
40   private Logger logger = LoggerFactory
41           .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
42
43   private StructureManager structureManager;
44
45   public ChimeraManager(StructureManager structureManager)
46   {
47     this.structureManager = structureManager;
48     chimera = null;
49     chimeraListenerThread = null;
50     currentModelsMap = new HashMap<Integer, ChimeraModel>();
51
52   }
53
54   public List<ChimeraModel> getChimeraModels(String modelName)
55   {
56     List<ChimeraModel> models = getChimeraModels(modelName,
57             ModelType.PDB_MODEL);
58     models.addAll(getChimeraModels(modelName, ModelType.SMILES));
59     return models;
60   }
61
62   public List<ChimeraModel> getChimeraModels(String modelName,
63           ModelType modelType)
64   {
65     List<ChimeraModel> models = new ArrayList<ChimeraModel>();
66     for (ChimeraModel model : currentModelsMap.values())
67     {
68       if (modelName.equals(model.getModelName())
69               && modelType.equals(model.getModelType()))
70       {
71         models.add(model);
72       }
73     }
74     return models;
75   }
76
77   public Map<String, List<ChimeraModel>> getChimeraModelsMap()
78   {
79     Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
80     for (ChimeraModel model : currentModelsMap.values())
81     {
82       String modelName = model.getModelName();
83       if (!models.containsKey(modelName))
84       {
85         models.put(modelName, new ArrayList<ChimeraModel>());
86       }
87       if (!models.get(modelName).contains(model))
88       {
89         models.get(modelName).add(model);
90       }
91     }
92     return models;
93   }
94
95   public ChimeraModel getChimeraModel(Integer modelNumber,
96           Integer subModelNumber)
97   {
98     Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
99     if (currentModelsMap.containsKey(key))
100     {
101       return currentModelsMap.get(key);
102     }
103     return null;
104   }
105
106   public ChimeraModel getChimeraModel()
107   {
108     return currentModelsMap.values().iterator().next();
109   }
110
111   public Collection<ChimeraModel> getChimeraModels()
112   {
113     // this method is invoked by the model navigator dialog
114     return currentModelsMap.values();
115   }
116
117   public int getChimeraModelsCount(boolean smiles)
118   {
119     // this method is invokes by the model navigator dialog
120     int counter = currentModelsMap.size();
121     if (smiles)
122     {
123       return counter;
124     }
125
126     for (ChimeraModel model : currentModelsMap.values())
127     {
128       if (model.getModelType() == ModelType.SMILES)
129       {
130         counter--;
131       }
132     }
133     return counter;
134   }
135
136   public boolean hasChimeraModel(Integer modelNubmer)
137   {
138     return hasChimeraModel(modelNubmer, 0);
139   }
140
141   public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
142   {
143     return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
144             subModelNumber));
145   }
146
147   public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
148           ChimeraModel model)
149   {
150     currentModelsMap.put(
151             ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
152   }
153
154   public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
155   {
156     int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
157     if (currentModelsMap.containsKey(modelKey))
158     {
159       currentModelsMap.remove(modelKey);
160     }
161   }
162
163   public List<ChimeraModel> openModel(String modelPath, ModelType type)
164   {
165     return openModel(modelPath, getFileNameFromPath(modelPath), type);
166   }
167
168   /**
169    * Overloaded method to allow Jalview to pass in a model name.
170    * 
171    * @param modelPath
172    * @param modelName
173    * @param type
174    * @return
175    */
176   public List<ChimeraModel> openModel(String modelPath, String modelName,
177           ModelType type)
178   {
179     logger.info("chimera open " + modelPath);
180     // stopListening();
181     List<String> response = null;
182     // TODO: [Optional] Handle modbase models
183     if (type == ModelType.MODBASE_MODEL)
184     {
185       response = sendChimeraCommand("open modbase:" + modelPath, true);
186       // } else if (type == ModelType.SMILES) {
187       // response = sendChimeraCommand("open smiles:" + modelName, true);
188       // modelName = "smiles:" + modelName;
189     }
190     else
191     {
192       response = sendChimeraCommand("open " + modelPath, true);
193     }
194     if (response == null)
195     {
196       // something went wrong
197       logger.warn("Could not open " + modelPath);
198       return null;
199     }
200     List<ChimeraModel> chimeraModels = getModelList();
201     // assign color and residues to open models
202     for (ChimeraModel chimeraModel : chimeraModels)
203     {
204       // // patch for Jalview - set model name in Chimera
205       // // TODO: find a variant that works for sub-models
206       chimeraModel.setModelName(modelName);
207       sendChimeraCommand(
208               "setattr M name " + modelName + " #"
209                       + chimeraModel.getModelNumber(), false);
210       // get model color
211       Color modelColor = getModelColor(chimeraModel);
212       if (modelColor != null)
213       {
214         chimeraModel.setModelColor(modelColor);
215       }
216
217       // Get our properties (default color scheme, etc.)
218       // Make the molecule look decent
219       // chimeraSend("repr stick "+newModel.toSpec());
220
221       // Create the information we need for the navigator
222       if (type != ModelType.SMILES)
223       {
224         addResidues(chimeraModel);
225       }
226     }
227
228     sendChimeraCommand("focus", false);
229     // startListening(); // see ChimeraListener
230     return chimeraModels;
231   }
232
233   /**
234    * Refactored method to extract the last (or only) element delimited by file
235    * path separator.
236    * 
237    * @param modelPath
238    * @return
239    */
240   private String getFileNameFromPath(String modelPath)
241   {
242     String modelName = modelPath;
243     if (modelPath == null)
244     {
245       return null;
246     }
247     // TODO: [Optional] Convert path to name in a better way
248     if (modelPath.lastIndexOf(File.separator) > 0)
249     {
250       modelName = modelPath
251               .substring(modelPath.lastIndexOf(File.separator) + 1);
252     }
253     else if (modelPath.lastIndexOf("/") > 0)
254     {
255       modelName = modelPath.substring(modelPath.lastIndexOf("/") + 1);
256     }
257     return modelName;
258   }
259
260   public void closeModel(ChimeraModel model)
261   {
262     // int model = structure.modelNumber();
263     // int subModel = structure.subModelNumber();
264     // Integer modelKey = makeModelKey(model, subModel);
265     stopListening();
266     logger.info("chimera close model " + model.getModelName());
267     if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
268             model.getModelNumber(), model.getSubModelNumber())))
269     {
270       sendChimeraCommand("close " + model.toSpec(), false);
271       // currentModelNamesMap.remove(model.getModelName());
272       currentModelsMap.remove(ChimUtils.makeModelKey(
273               model.getModelNumber(), model.getSubModelNumber()));
274       // selectionList.remove(chimeraModel);
275     }
276     else
277     {
278       logger.warn("Could not find model " + model.getModelName()
279               + " to close.");
280     }
281     startListening();
282   }
283
284   public void startListening()
285   {
286     sendChimeraCommand("listen start models; listen start selection", false);
287   }
288
289   public void stopListening()
290   {
291     sendChimeraCommand("listen stop models ; listen stop selection ", false);
292   }
293
294   /**
295    * Tell Chimera we are listening on the given URI
296    * 
297    * @param uri
298    */
299   public void startListening(String uri)
300   {
301     sendChimeraCommand("listen start models url " + uri
302             + ";listen start select prefix SelectionChanged url " + uri,
303             false);
304   }
305
306   /**
307    * Select something in Chimera
308    * 
309    * @param command
310    *          the selection command to pass to Chimera
311    */
312   public void select(String command)
313   {
314     sendChimeraCommand("listen stop selection; " + command
315             + "; listen start selection", false);
316   }
317
318   public void focus()
319   {
320     sendChimeraCommand("focus", false);
321   }
322
323   public void clearOnChimeraExit()
324   {
325     chimera = null;
326     currentModelsMap.clear();
327     this.chimeraRestPort = 0;
328     structureManager.clearOnChimeraExit();
329   }
330
331   public void exitChimera()
332   {
333     if (isChimeraLaunched() && chimera != null)
334     {
335       sendChimeraCommand("stop really", false);
336       try
337       {
338         chimera.destroy();
339       } catch (Exception ex)
340       {
341         // ignore
342       }
343     }
344     clearOnChimeraExit();
345   }
346
347   public Map<Integer, ChimeraModel> getSelectedModels()
348   {
349     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
350     List<String> chimeraReply = sendChimeraCommand(
351             "list selection level molecule", true);
352     if (chimeraReply != null)
353     {
354       for (String modelLine : chimeraReply)
355       {
356         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
357         Integer modelKey = ChimUtils.makeModelKey(
358                 chimeraModel.getModelNumber(),
359                 chimeraModel.getSubModelNumber());
360         selectedModelsMap.put(modelKey, chimeraModel);
361       }
362     }
363     return selectedModelsMap;
364   }
365
366   /**
367    * Sends a 'list selection level residue' command to Chimera and returns the
368    * list of selected atomspecs
369    * 
370    * @return
371    */
372   public List<String> getSelectedResidueSpecs()
373   {
374     List<String> selectedResidues = new ArrayList<String>();
375     List<String> chimeraReply = sendChimeraCommand(
376             "list selection level residue", true);
377     if (chimeraReply != null)
378     {
379       for (String inputLine : chimeraReply)
380       {
381         String[] inputLineParts = inputLine.split("\\s+");
382         if (inputLineParts.length == 5)
383         {
384           selectedResidues.add(inputLineParts[2]);
385         }
386       }
387     }
388     return selectedResidues;
389   }
390
391   public void getSelectedResidues(
392           Map<Integer, ChimeraModel> selectedModelsMap)
393   {
394     List<String> chimeraReply = sendChimeraCommand(
395             "list selection level residue", true);
396     if (chimeraReply != null)
397     {
398       for (String inputLine : chimeraReply)
399       {
400         ChimeraResidue r = new ChimeraResidue(inputLine);
401         Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
402                 r.getSubModelNumber());
403         if (selectedModelsMap.containsKey(modelKey))
404         {
405           ChimeraModel model = selectedModelsMap.get(modelKey);
406           model.addResidue(r);
407         }
408       }
409     }
410   }
411
412   /**
413    * Return the list of ChimeraModels currently open. Warning: if smiles model
414    * name too long, only part of it with "..." is printed.
415    * 
416    * 
417    * @return List of ChimeraModel's
418    */
419   // TODO: [Optional] Handle smiles names in a better way in Chimera?
420   public List<ChimeraModel> getModelList()
421   {
422     List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
423     List<String> list = sendChimeraCommand("list models type molecule",
424             true);
425     if (list != null)
426     {
427       for (String modelLine : list)
428       {
429         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
430         modelList.add(chimeraModel);
431       }
432     }
433     return modelList;
434   }
435
436   /**
437    * Return the list of depiction presets available from within Chimera. Chimera
438    * will return the list as a series of lines with the format: Preset type
439    * number "description"
440    * 
441    * @return list of presets
442    */
443   public List<String> getPresets()
444   {
445     ArrayList<String> presetList = new ArrayList<String>();
446     List<String> output = sendChimeraCommand("preset list", true);
447     if (output != null)
448     {
449       for (String preset : output)
450       {
451         preset = preset.substring(7); // Skip over the "Preset"
452         preset = preset.replaceFirst("\"", "(");
453         preset = preset.replaceFirst("\"", ")");
454         // string now looks like: type number (description)
455         presetList.add(preset);
456       }
457     }
458     return presetList;
459   }
460
461   public boolean isChimeraLaunched()
462   {
463     boolean launched = false;
464     if (chimera != null)
465     {
466       try
467       {
468         chimera.exitValue();
469         // if we get here, process has ended
470       } catch (IllegalThreadStateException e)
471       {
472         // ok - not yet terminated
473         launched = true;
474       }
475     }
476     return launched;
477   }
478
479   /**
480    * Launch Chimera, unless an instance linked to this object is already
481    * running. Returns true if chimera is successfully launched, or already
482    * running, else false.
483    * 
484    * @param chimeraPaths
485    * @return
486    */
487   public boolean launchChimera(List<String> chimeraPaths)
488   {
489     // Do nothing if Chimera is already launched
490     if (isChimeraLaunched())
491     {
492       return true;
493     }
494
495     // Try to launch Chimera (eventually using one of the possible paths)
496     String error = "Error message: ";
497     String workingPath = "";
498     // iterate over possible paths for starting Chimera
499     for (String chimeraPath : chimeraPaths)
500     {
501       File path = new File(chimeraPath);
502       // uncomment the next line to simulate Chimera not installed
503       // path = new File(chimeraPath + "x");
504       if (!path.canExecute())
505       {
506         error += "File '" + path + "' does not exist.\n";
507         continue;
508       }
509       try
510       {
511         List<String> args = new ArrayList<String>();
512         args.add(chimeraPath);
513         // shows Chimera output window but suppresses REST responses:
514         // args.add("--debug");
515         args.add("--start");
516         args.add("RESTServer");
517         ProcessBuilder pb = new ProcessBuilder(args);
518         chimera = pb.start();
519         error = "";
520         workingPath = chimeraPath;
521         break;
522       } catch (Exception e)
523       {
524         // Chimera could not be started
525         error += e.getMessage();
526       }
527     }
528     // If no error, then Chimera was launched successfully
529     if (error.length() == 0)
530     {
531       this.chimeraRestPort = getPortNumber();
532       System.out.println("Chimera REST API started on port "
533               + chimeraRestPort);
534       // structureManager.initChimTable();
535       structureManager.setChimeraPathProperty(workingPath);
536       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
537       // Ask Chimera to give us updates
538       // startListening(); // later - see ChimeraListener
539       return (chimeraRestPort > 0);
540     }
541
542     // Tell the user that Chimera could not be started because of an error
543     logger.warn(error);
544     return false;
545   }
546
547   /**
548    * Read and return the port number returned in the reply to --start RESTServer
549    */
550   private int getPortNumber()
551   {
552     int port = 0;
553     InputStream readChan = chimera.getInputStream();
554     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
555             readChan));
556     StringBuilder responses = new StringBuilder();
557     try
558     {
559       String response = lineReader.readLine();
560       while (response != null)
561       {
562         responses.append("\n" + response);
563         // expect: REST server on host 127.0.0.1 port port_number
564         if (response.startsWith("REST server"))
565         {
566           String[] tokens = response.split(" ");
567           if (tokens.length == 7 && "port".equals(tokens[5]))
568           {
569             port = Integer.parseInt(tokens[6]);
570             break;
571           }
572         }
573         response = lineReader.readLine();
574       }
575     } catch (Exception e)
576     {
577       logger.error("Failed to get REST port number from " + responses
578               + ": " + e.getMessage());
579     } finally
580     {
581       try
582       {
583         lineReader.close();
584       } catch (IOException e2)
585       {
586       }
587     }
588     if (port == 0)
589     {
590       System.err
591               .println("Failed to start Chimera with REST service, response was: "
592                       + responses);
593     }
594     logger.info("Chimera REST service listening on port " + chimeraRestPort);
595     return port;
596   }
597
598   /**
599    * Determine the color that Chimera is using for this model.
600    * 
601    * @param model
602    *          the ChimeraModel we want to get the Color for
603    * @return the default model Color for this model in Chimera
604    */
605   public Color getModelColor(ChimeraModel model)
606   {
607     List<String> colorLines = sendChimeraCommand(
608             "list model spec " + model.toSpec() + " attribute color", true);
609     if (colorLines == null || colorLines.size() == 0)
610     {
611       return null;
612     }
613     return ChimUtils.parseModelColor(colorLines.get(0));
614   }
615
616   /**
617    * 
618    * Get information about the residues associated with a model. This uses the
619    * Chimera listr command. We don't return the resulting residues, but we add
620    * the residues to the model.
621    * 
622    * @param model
623    *          the ChimeraModel to get residue information for
624    * 
625    */
626   public void addResidues(ChimeraModel model)
627   {
628     int modelNumber = model.getModelNumber();
629     int subModelNumber = model.getSubModelNumber();
630     // Get the list -- it will be in the reply log
631     List<String> reply = sendChimeraCommand(
632             "list residues spec " + model.toSpec(), true);
633     if (reply == null)
634     {
635       return;
636     }
637     for (String inputLine : reply)
638     {
639       ChimeraResidue r = new ChimeraResidue(inputLine);
640       if (r.getModelNumber() == modelNumber
641               || r.getSubModelNumber() == subModelNumber)
642       {
643         model.addResidue(r);
644       }
645     }
646   }
647
648   public List<String> getAttrList()
649   {
650     List<String> attributes = new ArrayList<String>();
651     final List<String> reply = sendChimeraCommand("list resattr", true);
652     if (reply != null)
653     {
654       for (String inputLine : reply)
655       {
656         String[] lineParts = inputLine.split("\\s");
657         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
658         {
659           attributes.add(lineParts[1]);
660         }
661       }
662     }
663     return attributes;
664   }
665
666   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
667           ChimeraModel model)
668   {
669     Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
670     final List<String> reply = sendChimeraCommand("list residue spec "
671             + model.toSpec() + " attribute " + aCommand, true);
672     if (reply != null)
673     {
674       for (String inputLine : reply)
675       {
676         String[] lineParts = inputLine.split("\\s");
677         if (lineParts.length == 5)
678         {
679           ChimeraResidue residue = ChimUtils
680                   .getResidue(lineParts[2], model);
681           String value = lineParts[4];
682           if (residue != null)
683           {
684             if (value.equals("None"))
685             {
686               continue;
687             }
688             if (value.equals("True") || value.equals("False"))
689             {
690               values.put(residue, Boolean.valueOf(value));
691               continue;
692             }
693             try
694             {
695               Double doubleValue = Double.valueOf(value);
696               values.put(residue, doubleValue);
697             } catch (NumberFormatException ex)
698             {
699               values.put(residue, value);
700             }
701           }
702         }
703       }
704     }
705     return values;
706   }
707
708   private volatile boolean busy = false;
709
710   /**
711    * Send a command to Chimera.
712    * 
713    * @param command
714    *          Command string to be send.
715    * @param reply
716    *          Flag indicating whether the method should return the reply from
717    *          Chimera or not.
718    * @return List of Strings corresponding to the lines in the Chimera reply or
719    *         <code>null</code>.
720    */
721   public List<String> sendChimeraCommand(String command, boolean reply)
722   {
723     // System.out.println("chimeradebug>> " + command);
724     if (!isChimeraLaunched() || command == null
725             || "".equals(command.trim()))
726     {
727       return null;
728     }
729     // TODO do we need a maximum wait time before aborting?
730     while (busy)
731     {
732       try
733       {
734         Thread.sleep(25);
735       } catch (InterruptedException q)
736       {
737       }
738     }
739     busy = true;
740     long startTime = System.currentTimeMillis();
741     try
742     {
743       return sendRestCommand(command);
744     } finally
745     {
746       /*
747        * Make sure busy flag is reset come what may!
748        */
749       busy = false;
750       if (debug)
751       {
752         System.out.println("Chimera command took "
753                 + (System.currentTimeMillis() - startTime) + "ms: "
754                 + command);
755       }
756
757     }
758   }
759
760   /**
761    * Sends the command to Chimera's REST API, and returns any response lines.
762    * 
763    * @param command
764    * @return
765    */
766   protected List<String> sendRestCommand(String command)
767   {
768     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
769     List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
770     commands.add(new BasicNameValuePair("command", command));
771
772     List<String> reply = new ArrayList<String>();
773     BufferedReader response = null;
774     try
775     {
776       response = HttpClientUtils
777               .doHttpUrlPost(restUrl, commands, 100, 5000);
778       String line = "";
779       while ((line = response.readLine()) != null)
780       {
781         reply.add(line);
782       }
783     } catch (Exception e)
784     {
785       logger.error("REST call '" + command + "' failed: " + e.getMessage());
786     } finally
787     {
788       if (response != null)
789       {
790         try
791         {
792           response.close();
793         } catch (IOException e)
794         {
795         }
796       }
797     }
798     return reply;
799   }
800
801   /**
802    * Send a command to stdin of Chimera process, and optionally read any
803    * responses.
804    * 
805    * @param command
806    * @param readReply
807    * @return
808    */
809   protected List<String> sendStdinCommand(String command, boolean readReply)
810   {
811     chimeraListenerThread.clearResponse(command);
812     String text = command.concat("\n");
813     try
814     {
815       // send the command
816       chimera.getOutputStream().write(text.getBytes());
817       chimera.getOutputStream().flush();
818     } catch (IOException e)
819     {
820       // logger.info("Unable to execute command: " + text);
821       // logger.info("Exiting...");
822       logger.warn("Unable to execute command: " + text);
823       logger.warn("Exiting...");
824       clearOnChimeraExit();
825       return null;
826     }
827     if (!readReply)
828     {
829       return null;
830     }
831     List<String> rsp = chimeraListenerThread.getResponse(command);
832     return rsp;
833   }
834
835   public StructureManager getStructureManager()
836   {
837     return structureManager;
838   }
839
840   public boolean isBusy()
841   {
842     return busy;
843   }
844 }