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