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