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