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