JAL-2320 thread to close Chimera viewer panel if Chimera shut down
[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         // TODO is this too violent? could it force close the process
352         // before it has done an orderly shutdown?
353         chimera.destroy();
354       } catch (Exception ex)
355       {
356         // ignore
357       }
358     }
359     clearOnChimeraExit();
360   }
361
362   public Map<Integer, ChimeraModel> getSelectedModels()
363   {
364     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
365     List<String> chimeraReply = sendChimeraCommand(
366             "list selection level molecule", true);
367     if (chimeraReply != null)
368     {
369       for (String modelLine : chimeraReply)
370       {
371         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
372         Integer modelKey = ChimUtils.makeModelKey(
373                 chimeraModel.getModelNumber(),
374                 chimeraModel.getSubModelNumber());
375         selectedModelsMap.put(modelKey, chimeraModel);
376       }
377     }
378     return selectedModelsMap;
379   }
380
381   /**
382    * Sends a 'list selection level residue' command to Chimera and returns the
383    * list of selected atomspecs
384    * 
385    * @return
386    */
387   public List<String> getSelectedResidueSpecs()
388   {
389     List<String> selectedResidues = new ArrayList<String>();
390     List<String> chimeraReply = sendChimeraCommand(
391             "list selection level residue", true);
392     if (chimeraReply != null)
393     {
394       for (String inputLine : chimeraReply)
395       {
396         String[] inputLineParts = inputLine.split("\\s+");
397         if (inputLineParts.length == 5)
398         {
399           selectedResidues.add(inputLineParts[2]);
400         }
401       }
402     }
403     return selectedResidues;
404   }
405
406   public void getSelectedResidues(
407           Map<Integer, ChimeraModel> selectedModelsMap)
408   {
409     List<String> chimeraReply = sendChimeraCommand(
410             "list selection level residue", true);
411     if (chimeraReply != null)
412     {
413       for (String inputLine : chimeraReply)
414       {
415         ChimeraResidue r = new ChimeraResidue(inputLine);
416         Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
417                 r.getSubModelNumber());
418         if (selectedModelsMap.containsKey(modelKey))
419         {
420           ChimeraModel model = selectedModelsMap.get(modelKey);
421           model.addResidue(r);
422         }
423       }
424     }
425   }
426
427   /**
428    * Return the list of ChimeraModels currently open. Warning: if smiles model
429    * name too long, only part of it with "..." is printed.
430    * 
431    * 
432    * @return List of ChimeraModel's
433    */
434   // TODO: [Optional] Handle smiles names in a better way in Chimera?
435   public List<ChimeraModel> getModelList()
436   {
437     List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
438     List<String> list = sendChimeraCommand("list models type molecule",
439             true);
440     if (list != null)
441     {
442       for (String modelLine : list)
443       {
444         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
445         modelList.add(chimeraModel);
446       }
447     }
448     return modelList;
449   }
450
451   /**
452    * Return the list of depiction presets available from within Chimera. Chimera
453    * will return the list as a series of lines with the format: Preset type
454    * number "description"
455    * 
456    * @return list of presets
457    */
458   public List<String> getPresets()
459   {
460     ArrayList<String> presetList = new ArrayList<String>();
461     List<String> output = sendChimeraCommand("preset list", true);
462     if (output != null)
463     {
464       for (String preset : output)
465       {
466         preset = preset.substring(7); // Skip over the "Preset"
467         preset = preset.replaceFirst("\"", "(");
468         preset = preset.replaceFirst("\"", ")");
469         // string now looks like: type number (description)
470         presetList.add(preset);
471       }
472     }
473     return presetList;
474   }
475
476   public boolean isChimeraLaunched()
477   {
478     boolean launched = false;
479     if (chimera != null)
480     {
481       try
482       {
483         chimera.exitValue();
484         // if we get here, process has ended
485       } catch (IllegalThreadStateException e)
486       {
487         // ok - not yet terminated
488         launched = true;
489       }
490     }
491     return launched;
492   }
493
494   /**
495    * Launch Chimera, unless an instance linked to this object is already
496    * running. Returns true if chimera is successfully launched, or already
497    * running, else false.
498    * 
499    * @param chimeraPaths
500    * @return
501    */
502   public boolean launchChimera(List<String> chimeraPaths)
503   {
504     // Do nothing if Chimera is already launched
505     if (isChimeraLaunched())
506     {
507       return true;
508     }
509
510     // Try to launch Chimera (eventually using one of the possible paths)
511     String error = "Error message: ";
512     String workingPath = "";
513     // iterate over possible paths for starting Chimera
514     for (String chimeraPath : chimeraPaths)
515     {
516       File path = new File(chimeraPath);
517       // uncomment the next line to simulate Chimera not installed
518       // path = new File(chimeraPath + "x");
519       if (!path.canExecute())
520       {
521         error += "File '" + path + "' does not exist.\n";
522         continue;
523       }
524       try
525       {
526         List<String> args = new ArrayList<String>();
527         args.add(chimeraPath);
528         // shows Chimera output window but suppresses REST responses:
529         // args.add("--debug");
530         args.add("--start");
531         args.add("RESTServer");
532         ProcessBuilder pb = new ProcessBuilder(args);
533         chimera = pb.start();
534         error = "";
535         workingPath = chimeraPath;
536         break;
537       } catch (Exception e)
538       {
539         // Chimera could not be started
540         error += e.getMessage();
541       }
542     }
543     // If no error, then Chimera was launched successfully
544     if (error.length() == 0)
545     {
546       this.chimeraRestPort = getPortNumber();
547       System.out.println("Chimera REST API started on port "
548               + chimeraRestPort);
549       // structureManager.initChimTable();
550       structureManager.setChimeraPathProperty(workingPath);
551       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
552       // Ask Chimera to give us updates
553       // startListening(); // later - see ChimeraListener
554       return (chimeraRestPort > 0);
555     }
556
557     // Tell the user that Chimera could not be started because of an error
558     logger.warn(error);
559     return false;
560   }
561
562   /**
563    * Read and return the port number returned in the reply to --start RESTServer
564    */
565   private int getPortNumber()
566   {
567     int port = 0;
568     InputStream readChan = chimera.getInputStream();
569     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
570             readChan));
571     StringBuilder responses = new StringBuilder();
572     try
573     {
574       String response = lineReader.readLine();
575       while (response != null)
576       {
577         responses.append("\n" + response);
578         // expect: REST server on host 127.0.0.1 port port_number
579         if (response.startsWith("REST server"))
580         {
581           String[] tokens = response.split(" ");
582           if (tokens.length == 7 && "port".equals(tokens[5]))
583           {
584             port = Integer.parseInt(tokens[6]);
585             break;
586           }
587         }
588         response = lineReader.readLine();
589       }
590     } catch (Exception e)
591     {
592       logger.error("Failed to get REST port number from " + responses
593               + ": " + e.getMessage());
594     } finally
595     {
596       try
597       {
598         lineReader.close();
599       } catch (IOException e2)
600       {
601       }
602     }
603     if (port == 0)
604     {
605       System.err
606               .println("Failed to start Chimera with REST service, response was: "
607                       + responses);
608     }
609     logger.info("Chimera REST service listening on port " + chimeraRestPort);
610     return port;
611   }
612
613   /**
614    * Determine the color that Chimera is using for this model.
615    * 
616    * @param model
617    *          the ChimeraModel we want to get the Color for
618    * @return the default model Color for this model in Chimera
619    */
620   public Color getModelColor(ChimeraModel model)
621   {
622     List<String> colorLines = sendChimeraCommand(
623             "list model spec " + model.toSpec() + " attribute color", true);
624     if (colorLines == null || colorLines.size() == 0)
625     {
626       return null;
627     }
628     return ChimUtils.parseModelColor(colorLines.get(0));
629   }
630
631   /**
632    * 
633    * Get information about the residues associated with a model. This uses the
634    * Chimera listr command. We don't return the resulting residues, but we add
635    * the residues to the model.
636    * 
637    * @param model
638    *          the ChimeraModel to get residue information for
639    * 
640    */
641   public void addResidues(ChimeraModel model)
642   {
643     int modelNumber = model.getModelNumber();
644     int subModelNumber = model.getSubModelNumber();
645     // Get the list -- it will be in the reply log
646     List<String> reply = sendChimeraCommand(
647             "list residues spec " + model.toSpec(), true);
648     if (reply == null)
649     {
650       return;
651     }
652     for (String inputLine : reply)
653     {
654       ChimeraResidue r = new ChimeraResidue(inputLine);
655       if (r.getModelNumber() == modelNumber
656               || r.getSubModelNumber() == subModelNumber)
657       {
658         model.addResidue(r);
659       }
660     }
661   }
662
663   public List<String> getAttrList()
664   {
665     List<String> attributes = new ArrayList<String>();
666     final List<String> reply = sendChimeraCommand("list resattr", true);
667     if (reply != null)
668     {
669       for (String inputLine : reply)
670       {
671         String[] lineParts = inputLine.split("\\s");
672         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
673         {
674           attributes.add(lineParts[1]);
675         }
676       }
677     }
678     return attributes;
679   }
680
681   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
682           ChimeraModel model)
683   {
684     Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
685     final List<String> reply = sendChimeraCommand("list residue spec "
686             + model.toSpec() + " attribute " + aCommand, true);
687     if (reply != null)
688     {
689       for (String inputLine : reply)
690       {
691         String[] lineParts = inputLine.split("\\s");
692         if (lineParts.length == 5)
693         {
694           ChimeraResidue residue = ChimUtils
695                   .getResidue(lineParts[2], model);
696           String value = lineParts[4];
697           if (residue != null)
698           {
699             if (value.equals("None"))
700             {
701               continue;
702             }
703             if (value.equals("True") || value.equals("False"))
704             {
705               values.put(residue, Boolean.valueOf(value));
706               continue;
707             }
708             try
709             {
710               Double doubleValue = Double.valueOf(value);
711               values.put(residue, doubleValue);
712             } catch (NumberFormatException ex)
713             {
714               values.put(residue, value);
715             }
716           }
717         }
718       }
719     }
720     return values;
721   }
722
723   private volatile boolean busy = false;
724
725   /**
726    * Send a command to Chimera.
727    * 
728    * @param command
729    *          Command string to be send.
730    * @param reply
731    *          Flag indicating whether the method should return the reply from
732    *          Chimera or not.
733    * @return List of Strings corresponding to the lines in the Chimera reply or
734    *         <code>null</code>.
735    */
736   public List<String> sendChimeraCommand(String command, boolean reply)
737   {
738    // System.out.println("chimeradebug>> " + command);
739     if (!isChimeraLaunched() || command == null
740             || "".equals(command.trim()))
741     {
742       return null;
743     }
744     // TODO do we need a maximum wait time before aborting?
745     while (busy)
746     {
747       try
748       {
749         Thread.sleep(25);
750       } catch (InterruptedException q)
751       {
752       }
753     }
754     busy = true;
755     long startTime = System.currentTimeMillis();
756     try
757     {
758       return sendRestCommand(command);
759     } finally
760     {
761       /*
762        * Make sure busy flag is reset come what may!
763        */
764       busy = false;
765       if (debug)
766       {
767         System.out.println("Chimera command took "
768                 + (System.currentTimeMillis() - startTime) + "ms: "
769                 + command);
770       }
771
772     }
773   }
774
775   /**
776    * Sends the command to Chimera's REST API, and returns any response lines.
777    * 
778    * @param command
779    * @return
780    */
781   protected List<String> sendRestCommand(String command)
782   {
783     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
784     List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
785     commands.add(new BasicNameValuePair("command", command));
786
787     List<String> reply = new ArrayList<String>();
788     BufferedReader response = null;
789     try
790     {
791       response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS,
792               REST_REPLY_TIMEOUT_MS);
793       String line = "";
794       while ((line = response.readLine()) != null)
795       {
796         reply.add(line);
797       }
798     } catch (Exception e)
799     {
800       logger.error("REST call '" + command + "' failed: " + e.getMessage());
801     } finally
802     {
803       if (response != null)
804       {
805         try
806         {
807           response.close();
808         } catch (IOException e)
809         {
810         }
811       }
812     }
813     return reply;
814   }
815
816   /**
817    * Send a command to stdin of Chimera process, and optionally read any
818    * responses.
819    * 
820    * @param command
821    * @param readReply
822    * @return
823    */
824   protected List<String> sendStdinCommand(String command, boolean readReply)
825   {
826     chimeraListenerThread.clearResponse(command);
827     String text = command.concat("\n");
828     try
829     {
830       // send the command
831       chimera.getOutputStream().write(text.getBytes());
832       chimera.getOutputStream().flush();
833     } catch (IOException e)
834     {
835       // logger.info("Unable to execute command: " + text);
836       // logger.info("Exiting...");
837       logger.warn("Unable to execute command: " + text);
838       logger.warn("Exiting...");
839       clearOnChimeraExit();
840       return null;
841     }
842     if (!readReply)
843     {
844       return null;
845     }
846     List<String> rsp = chimeraListenerThread.getResponse(command);
847     return rsp;
848   }
849
850   public StructureManager getStructureManager()
851   {
852     return structureManager;
853   }
854
855   public boolean isBusy()
856   {
857     return busy;
858   }
859
860   public Process getChimeraProcess()
861   {
862     return chimera;
863   }
864 }