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