JAL-1333 more refinements - deadlocks still possible with this version
[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   static private Process chimera;
25
26   static private ListenerThreads chimeraListenerThreads;
27
28   static private Map<Integer, ChimeraModel> currentModelsMap;
29
30   private static 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     logger.info("chimera open " + modelPath);
156     stopListening();
157     List<String> response = null;
158     // TODO: [Optional] Handle modbase models
159     if (type == ModelType.MODBASE_MODEL)
160     {
161       response = sendChimeraCommand("open modbase:" + modelPath, true);
162       // } else if (type == ModelType.SMILES) {
163       // response = sendChimeraCommand("open smiles:" + modelName, true);
164       // modelName = "smiles:" + modelName;
165     }
166     else
167     {
168       response = sendChimeraCommand("open " + modelPath, true);
169     }
170     if (response == null)
171     {
172       // something went wrong
173       logger.warn("Could not open " + modelPath);
174       return null;
175     }
176     List<ChimeraModel> models = new ArrayList<ChimeraModel>();
177     int[] modelNumbers = null;
178     if (type == ModelType.PDB_MODEL)
179     {
180       for (String line : response)
181       {
182         if (line.startsWith("#"))
183         {
184           modelNumbers = ChimUtils.parseOpenedModelNumber(line);
185           if (modelNumbers != null)
186           {
187             int modelNumber = ChimUtils.makeModelKey(modelNumbers[0],
188                     modelNumbers[1]);
189             if (currentModelsMap.containsKey(modelNumber))
190             {
191               continue;
192             }
193             String modelName = modelPath;
194             // TODO: [Optional] Convert path to name in a better way
195             if (modelPath.lastIndexOf(File.separator) > 0)
196             {
197               modelName = modelPath.substring(modelPath
198                       .lastIndexOf(File.separator) + 1);
199             }
200             else if (modelPath.lastIndexOf("/") > 0)
201             {
202               modelName = modelPath
203                       .substring(modelPath.lastIndexOf("/") + 1);
204             }
205             ChimeraModel newModel = new ChimeraModel(modelName, type,
206                     modelNumbers[0], modelNumbers[1]);
207             currentModelsMap.put(modelNumber, newModel);
208             models.add(newModel);
209             modelNumbers = null;
210           }
211         }
212       }
213     }
214     else
215     {
216       // TODO: [Optional] Open smiles from file would fail. Do we need it?
217       // If parsing fails, iterate over all open models to get the right one
218       List<ChimeraModel> openModels = getModelList();
219       for (ChimeraModel openModel : openModels)
220       {
221         String openModelName = openModel.getModelName();
222         if (openModelName.endsWith("..."))
223         {
224           openModelName = openModelName.substring(0,
225                   openModelName.length() - 3);
226         }
227         if (modelPath.startsWith(openModelName))
228         {
229           openModel.setModelName(modelPath);
230           int modelNumber = ChimUtils
231                   .makeModelKey(openModel.getModelNumber(),
232                           openModel.getSubModelNumber());
233           if (!currentModelsMap.containsKey(modelNumber))
234           {
235             currentModelsMap.put(modelNumber, openModel);
236             models.add(openModel);
237           }
238         }
239       }
240     }
241
242     // assign color and residues to open models
243     for (ChimeraModel newModel : models)
244     {
245       // get model color
246       Color modelColor = getModelColor(newModel);
247       if (modelColor != null)
248       {
249         newModel.setModelColor(modelColor);
250       }
251
252       // Get our properties (default color scheme, etc.)
253       // Make the molecule look decent
254       // chimeraSend("repr stick "+newModel.toSpec());
255
256       // Create the information we need for the navigator
257       if (type != ModelType.SMILES)
258       {
259         addResidues(newModel);
260       }
261     }
262
263     sendChimeraCommand("focus", false);
264     startListening();
265     return models;
266   }
267
268   public void closeModel(ChimeraModel model)
269   {
270     // int model = structure.modelNumber();
271     // int subModel = structure.subModelNumber();
272     // Integer modelKey = makeModelKey(model, subModel);
273     stopListening();
274     logger.info("chimera close model " + model.getModelName());
275     if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
276             model.getModelNumber(), model.getSubModelNumber())))
277     {
278       sendChimeraCommand("close " + model.toSpec(), false);
279       // currentModelNamesMap.remove(model.getModelName());
280       currentModelsMap.remove(ChimUtils.makeModelKey(
281               model.getModelNumber(), model.getSubModelNumber()));
282       // selectionList.remove(chimeraModel);
283     }
284     else
285     {
286       logger.warn("Could not find model " + model.getModelName()
287               + " to close.");
288     }
289     startListening();
290   }
291
292   public void startListening()
293   {
294     sendChimeraCommand("listen start models; listen start select", false);
295   }
296
297   public void stopListening()
298   {
299     sendChimeraCommand("listen stop models; listen stop select", false);
300   }
301
302   /**
303    * Select something in Chimera
304    * 
305    * @param command
306    *          the selection command to pass to Chimera
307    */
308   public void select(String command)
309   {
310     sendChimeraCommand("listen stop select; " + command
311             + "; listen start select", false);
312   }
313
314   public void focus()
315   {
316     sendChimeraCommand("focus", false);
317   }
318
319   public void clearOnChimeraExit()
320   {
321     chimera = null;
322     currentModelsMap.clear();
323     chimeraListenerThreads = null;
324     structureManager.clearOnChimeraExit();
325   }
326
327   public void exitChimera()
328   {
329     if (isChimeraLaunched() && chimera != null)
330     {
331       sendChimeraCommand("stop really", false);
332       try
333       {
334         chimera.destroy();
335       } catch (Exception ex)
336       {
337         // ignore
338       }
339     }
340     clearOnChimeraExit();
341   }
342
343   public Map<Integer, ChimeraModel> getSelectedModels()
344   {
345     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
346     List<String> chimeraReply = sendChimeraCommand(
347             "list selection level molecule", true);
348     if (chimeraReply != null)
349     {
350       for (String modelLine : chimeraReply)
351       {
352         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
353         Integer modelKey = ChimUtils.makeModelKey(
354                 chimeraModel.getModelNumber(),
355                 chimeraModel.getSubModelNumber());
356         selectedModelsMap.put(modelKey, chimeraModel);
357       }
358     }
359     return selectedModelsMap;
360   }
361
362   public List<String> getSelectedResidueSpecs()
363   {
364     List<String> selectedResidues = new ArrayList<String>();
365     List<String> chimeraReply = sendChimeraCommand(
366             "list selection level residue", true);
367     if (chimeraReply != null)
368     {
369       for (String inputLine : chimeraReply)
370       {
371         String[] inputLineParts = inputLine.split("\\s+");
372         if (inputLineParts.length == 5)
373         {
374           selectedResidues.add(inputLineParts[2]);
375         }
376       }
377     }
378     return selectedResidues;
379   }
380
381   public void getSelectedResidues(
382           Map<Integer, ChimeraModel> selectedModelsMap)
383   {
384     List<String> chimeraReply = sendChimeraCommand(
385             "list selection level residue", true);
386     if (chimeraReply != null)
387     {
388       for (String inputLine : chimeraReply)
389       {
390         ChimeraResidue r = new ChimeraResidue(inputLine);
391         Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
392                 r.getSubModelNumber());
393         if (selectedModelsMap.containsKey(modelKey))
394         {
395           ChimeraModel model = selectedModelsMap.get(modelKey);
396           model.addResidue(r);
397         }
398       }
399     }
400   }
401
402   /**
403    * Return the list of ChimeraModels currently open. Warning: if smiles model
404    * name too long, only part of it with "..." is printed.
405    * 
406    * 
407    * @return List of ChimeraModel's
408    */
409   // TODO: [Optional] Handle smiles names in a better way in Chimera?
410   public List<ChimeraModel> getModelList()
411   {
412     List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
413     List<String> list = sendChimeraCommand("list models type molecule",
414             true);
415     if (list != null)
416     {
417       for (String modelLine : list)
418       {
419         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
420         modelList.add(chimeraModel);
421       }
422     }
423     return modelList;
424   }
425
426   /**
427    * Return the list of depiction presets available from within Chimera. Chimera
428    * will return the list as a series of lines with the format: Preset type
429    * number "description"
430    * 
431    * @return list of presets
432    */
433   public List<String> getPresets()
434   {
435     ArrayList<String> presetList = new ArrayList<String>();
436     List<String> output = sendChimeraCommand("preset list", true);
437     if (output != null)
438     {
439       for (String preset : output)
440       {
441         preset = preset.substring(7); // Skip over the "Preset"
442         preset = preset.replaceFirst("\"", "(");
443         preset = preset.replaceFirst("\"", ")");
444         // string now looks like: type number (description)
445         presetList.add(preset);
446       }
447     }
448     return presetList;
449   }
450
451   public boolean isChimeraLaunched()
452   {
453     // TODO: [Optional] What is the best way to test if chimera is launched?
454
455     // sendChimeraCommand("test", true) !=null
456     if (chimera != null)
457     {
458       return true;
459     }
460     return false;
461   }
462
463   public boolean launchChimera(List<String> chimeraPaths)
464   {
465     // Do nothing if Chimera is already launched
466     if (isChimeraLaunched())
467     {
468       return true;
469     }
470
471     // Try to launch Chimera (eventually using one of the possible paths)
472     String error = "Error message: ";
473     String workingPath = "";
474     // iterate over possible paths for starting Chimera
475     for (String chimeraPath : chimeraPaths)
476     {
477       File path = new File(chimeraPath);
478       if (!path.canExecute())
479       {
480         error += "File '" + path + "' does not exist.\n";
481         continue;
482       }
483       try
484       {
485         List<String> args = new ArrayList<String>();
486         args.add(chimeraPath);
487         args.add("--start");
488         args.add("ReadStdin");
489         ProcessBuilder pb = new ProcessBuilder(args);
490         chimera = pb.start();
491         error = "";
492         workingPath = chimeraPath;
493         logger.info("Strarting " + chimeraPath);
494         break;
495       } catch (Exception e)
496       {
497         // Chimera could not be started
498         error += e.getMessage();
499       }
500     }
501     // If no error, then Chimera was launched successfully
502     if (error.length() == 0)
503     {
504       // Initialize the listener threads
505       chimeraListenerThreads = new ListenerThreads(chimera,
506               structureManager);
507       chimeraListenerThreads.start();
508       // structureManager.initChimTable();
509       structureManager.setChimeraPathProperty(workingPath);
510       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
511       // Ask Chimera to give us updates
512       startListening();
513       return true;
514     }
515
516     // Tell the user that Chimera could not be started because of an error
517     logger.warn(error);
518     return false;
519   }
520
521   /**
522    * Determine the color that Chimera is using for this model.
523    * 
524    * @param model
525    *          the ChimeraModel we want to get the Color for
526    * @return the default model Color for this model in Chimera
527    */
528   public Color getModelColor(ChimeraModel model)
529   {
530     List<String> colorLines = sendChimeraCommand(
531             "list model spec " + model.toSpec() + " attribute color", true);
532     if (colorLines == null || colorLines.size() == 0)
533     {
534       return null;
535     }
536     return ChimUtils.parseModelColor((String) colorLines.get(0));
537   }
538
539   /**
540    * 
541    * Get information about the residues associated with a model. This uses the
542    * Chimera listr command. We don't return the resulting residues, but we add
543    * the residues to the model.
544    * 
545    * @param model
546    *          the ChimeraModel to get residue information for
547    * 
548    */
549   public void addResidues(ChimeraModel model)
550   {
551     int modelNumber = model.getModelNumber();
552     int subModelNumber = model.getSubModelNumber();
553     // Get the list -- it will be in the reply log
554     List<String> reply = sendChimeraCommand(
555             "list residues spec " + model.toSpec(), true);
556     if (reply == null)
557     {
558       return;
559     }
560     for (String inputLine : reply)
561     {
562       ChimeraResidue r = new ChimeraResidue(inputLine);
563       if (r.getModelNumber() == modelNumber
564               || r.getSubModelNumber() == subModelNumber)
565       {
566         model.addResidue(r);
567       }
568     }
569   }
570
571   public List<String> getAttrList()
572   {
573     List<String> attributes = new ArrayList<String>();
574     final List<String> reply = sendChimeraCommand("list resattr", true);
575     if (reply != null)
576     {
577       for (String inputLine : reply)
578       {
579         String[] lineParts = inputLine.split("\\s");
580         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
581         {
582           attributes.add(lineParts[1]);
583         }
584       }
585     }
586     return attributes;
587   }
588
589   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
590           ChimeraModel model)
591   {
592     Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
593     final List<String> reply = sendChimeraCommand("list residue spec "
594             + model.toSpec() + " attribute " + aCommand, true);
595     if (reply != null)
596     {
597       for (String inputLine : reply)
598       {
599         String[] lineParts = inputLine.split("\\s");
600         if (lineParts.length == 5)
601         {
602           ChimeraResidue residue = ChimUtils
603                   .getResidue(lineParts[2], model);
604           String value = lineParts[4];
605           if (residue != null)
606           {
607             if (value.equals("None"))
608             {
609               continue;
610             }
611             if (value.equals("True") || value.equals("False"))
612             {
613               values.put(residue, Boolean.valueOf(value));
614               continue;
615             }
616             try
617             {
618               Double doubleValue = Double.valueOf(value);
619               values.put(residue, doubleValue);
620             } catch (NumberFormatException ex)
621             {
622               values.put(residue, value);
623             }
624           }
625         }
626       }
627     }
628     return values;
629   }
630   private volatile boolean busy=false;
631   /**
632    * Send a command to Chimera.
633    * 
634    * @param command
635    *          Command string to be send.
636    * @param reply
637    *          Flag indicating whether the method should return the reply from
638    *          Chimera or not.
639    * @return List of Strings corresponding to the lines in the Chimera reply or
640    *         <code>null</code>.
641    */
642   public List<String> sendChimeraCommand(String command, boolean reply)
643   {
644     if (!isChimeraLaunched())
645     {
646       return null;
647     }
648     while (busy)
649     {
650       try {
651         Thread.sleep(25);
652       } catch (InterruptedException q) {};
653     }
654     busy=true;
655     chimeraListenerThreads.clearResponse(command);
656     String text = command.concat("\n");
657     // System.out.println("send command to chimera: " + text);
658     try
659     {
660       // send the command
661       chimera.getOutputStream().write(text.getBytes());
662       chimera.getOutputStream().flush();
663     } catch (IOException e)
664     {
665       // logger.info("Unable to execute command: " + text);
666       // logger.info("Exiting...");
667       logger.warn("Unable to execute command: " + text);
668       logger.warn("Exiting...");
669       clearOnChimeraExit();
670       busy=false;
671       return null;
672     }
673     if (!reply)
674     {
675       busy=false;
676       return null;
677     }
678     List<String> rsp = chimeraListenerThreads.getResponse(command);
679     busy=false;
680     return rsp;
681   }
682
683   public StructureManager getStructureManager()
684   {
685     return structureManager;
686   }
687
688 }