JAL-3518 more extraction of ChimeraX commands as overrides
[jalview.git] / src / jalview / gui / ChimeraViewFrame.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.api.AlignmentViewPanel;
24 import jalview.api.FeatureRenderer;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.PDBEntry;
28 import jalview.datamodel.SequenceI;
29 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
30 import jalview.gui.StructureViewer.ViewerType;
31 import jalview.io.DataSourceType;
32 import jalview.io.StructureFile;
33 import jalview.structures.models.AAStructureBindingModel;
34 import jalview.util.BrowserLauncher;
35 import jalview.util.MessageManager;
36 import jalview.util.Platform;
37 import jalview.ws.dbsources.Pdb;
38
39 import java.awt.event.ActionEvent;
40 import java.awt.event.ActionListener;
41 import java.awt.event.MouseAdapter;
42 import java.awt.event.MouseEvent;
43 import java.io.File;
44 import java.io.FileInputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.List;
50
51 import javax.swing.JInternalFrame;
52 import javax.swing.JMenu;
53 import javax.swing.JMenuItem;
54 import javax.swing.event.InternalFrameAdapter;
55 import javax.swing.event.InternalFrameEvent;
56
57 /**
58  * GUI elements for handling an external chimera display
59  * 
60  * @author jprocter
61  *
62  */
63 public class ChimeraViewFrame extends StructureViewerBase
64 {
65   private JalviewChimeraBinding jmb;
66
67   /*
68    * Path to Chimera session file. This is set when an open Jalview/Chimera
69    * session is saved, or on restore from a Jalview project (if it holds the
70    * filename of any saved Chimera sessions).
71    */
72   private String chimeraSessionFile = null;
73
74   private int myWidth = 500;
75
76   private int myHeight = 150;
77
78   /**
79    * Initialise menu options.
80    */
81   @Override
82   protected void initMenus()
83   {
84     super.initMenus();
85
86     viewerActionMenu.setText(MessageManager.getString("label.chimera"));
87
88     viewerColour
89             .setText(MessageManager.getString("label.colour_with_chimera"));
90     viewerColour.setToolTipText(MessageManager
91             .getString("label.let_chimera_manage_structure_colours"));
92
93     helpItem.setText(MessageManager.getString("label.chimera_help"));
94     savemenu.setVisible(false); // not yet implemented
95     viewMenu.add(fitToWindow);
96
97     JMenuItem writeFeatures = new JMenuItem(
98             MessageManager.getString("label.create_chimera_attributes"));
99     writeFeatures.setToolTipText(MessageManager
100             .getString("label.create_chimera_attributes_tip"));
101     writeFeatures.addActionListener(new ActionListener()
102     {
103       @Override
104       public void actionPerformed(ActionEvent e)
105       {
106         sendFeaturesToChimera();
107       }
108     });
109     viewerActionMenu.add(writeFeatures);
110
111     final JMenu fetchAttributes = new JMenu(
112             MessageManager.getString("label.fetch_chimera_attributes"));
113     fetchAttributes.setToolTipText(
114             MessageManager.getString("label.fetch_chimera_attributes_tip"));
115     fetchAttributes.addMouseListener(new MouseAdapter()
116     {
117
118       @Override
119       public void mouseEntered(MouseEvent e)
120       {
121         buildAttributesMenu(fetchAttributes);
122       }
123     });
124     viewerActionMenu.add(fetchAttributes);
125   }
126
127   /**
128    * Query Chimera for its residue attribute names and add them as items off the
129    * attributes menu
130    * 
131    * @param attributesMenu
132    */
133   protected void buildAttributesMenu(JMenu attributesMenu)
134   {
135     List<String> atts = jmb.getChimeraAttributes();
136     attributesMenu.removeAll();
137     Collections.sort(atts);
138     for (String attName : atts)
139     {
140       JMenuItem menuItem = new JMenuItem(attName);
141       menuItem.addActionListener(new ActionListener()
142       {
143         @Override
144         public void actionPerformed(ActionEvent e)
145         {
146           getChimeraAttributes(attName);
147         }
148       });
149       attributesMenu.add(menuItem);
150     }
151   }
152
153   /**
154    * Read residues in Chimera with the given attribute name, and set as features
155    * on the corresponding sequence positions (if any)
156    * 
157    * @param attName
158    */
159   protected void getChimeraAttributes(String attName)
160   {
161     jmb.copyStructureAttributesToFeatures(attName, getAlignmentPanel());
162   }
163
164   /**
165    * Send a command to Chimera to create residue attributes for Jalview features
166    * <p>
167    * The syntax is: setattr r &lt;attName&gt; &lt;attValue&gt; &lt;atomSpec&gt;
168    * <p>
169    * For example: setattr r jv_chain "Ferredoxin-1, Chloroplastic" #0:94.A
170    */
171   protected void sendFeaturesToChimera()
172   {
173     int count = jmb.sendFeaturesToViewer(getAlignmentPanel());
174     statusBar.setText(
175             MessageManager.formatMessage("label.attributes_set", count));
176   }
177
178   /**
179    * open a single PDB structure in a new Chimera view
180    * 
181    * @param pdbentry
182    * @param seq
183    * @param chains
184    * @param ap
185    */
186   public ChimeraViewFrame(PDBEntry pdbentry, SequenceI[] seq,
187           String[] chains, final AlignmentPanel ap)
188   {
189     this();
190
191     openNewChimera(ap, new PDBEntry[] { pdbentry },
192             new SequenceI[][]
193             { seq });
194   }
195
196   /**
197    * Create a helper to manage progress bar display
198    */
199   protected void createProgressBar()
200   {
201     if (getProgressIndicator() == null)
202     {
203       setProgressIndicator(new ProgressBar(statusPanel, statusBar));
204     }
205   }
206
207   private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
208           SequenceI[][] seqs)
209   {
210     createProgressBar();
211     jmb = new JalviewChimeraBindingModel(this,
212             ap.getStructureSelectionManager(), pdbentrys, seqs, null);
213     addAlignmentPanel(ap);
214     useAlignmentPanelForColourbyseq(ap);
215
216     if (pdbentrys.length > 1)
217     {
218       useAlignmentPanelForSuperposition(ap);
219     }
220     jmb.setColourBySequence(true);
221     setSize(myWidth, myHeight);
222     initMenus();
223
224     addingStructures = false;
225     worker = new Thread(this);
226     worker.start();
227
228     this.addInternalFrameListener(new InternalFrameAdapter()
229     {
230       @Override
231       public void internalFrameClosing(
232               InternalFrameEvent internalFrameEvent)
233       {
234         closeViewer(false);
235       }
236     });
237
238   }
239
240   /**
241    * Create a new viewer from saved session state data including Chimera session
242    * file
243    * 
244    * @param chimeraSessionFile
245    * @param alignPanel
246    * @param pdbArray
247    * @param seqsArray
248    * @param colourByChimera
249    * @param colourBySequence
250    * @param newViewId
251    */
252   public ChimeraViewFrame(String chimeraSessionFile,
253           AlignmentPanel alignPanel, PDBEntry[] pdbArray,
254           SequenceI[][] seqsArray, boolean colourByChimera,
255           boolean colourBySequence, String newViewId)
256   {
257     this();
258     setViewId(newViewId);
259     this.chimeraSessionFile = chimeraSessionFile;
260     openNewChimera(alignPanel, pdbArray, seqsArray);
261     if (colourByChimera)
262     {
263       jmb.setColourBySequence(false);
264       seqColour.setSelected(false);
265       viewerColour.setSelected(true);
266     }
267     else if (colourBySequence)
268     {
269       jmb.setColourBySequence(true);
270       seqColour.setSelected(true);
271       viewerColour.setSelected(false);
272     }
273   }
274
275   /**
276    * create a new viewer containing several structures, optionally superimposed
277    * using the given alignPanel.
278    * 
279    * @param pe
280    * @param seqs
281    * @param ap
282    */
283   public ChimeraViewFrame(PDBEntry[] pe, boolean alignAdded,
284           SequenceI[][] seqs,
285           AlignmentPanel ap)
286   {
287     this();
288     setAlignAddedStructures(alignAdded);
289     openNewChimera(ap, pe, seqs);
290   }
291
292   /**
293    * Default constructor
294    */
295   public ChimeraViewFrame()
296   {
297     super();
298
299     /*
300      * closeViewer will decide whether or not to close this frame
301      * depending on whether user chooses to Cancel or not
302      */
303     setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
304   }
305
306   /**
307    * Launch Chimera. If we have a chimera session file name, send Chimera the
308    * command to open its saved session file.
309    */
310   void initChimera()
311   {
312     jmb.setFinishedInit(false);
313     Desktop.addInternalFrame(this,
314             jmb.getViewerTitle(getViewerName(), true), getBounds().width,
315             getBounds().height);
316
317     if (!jmb.launchChimera())
318     {
319       JvOptionPane.showMessageDialog(Desktop.desktop,
320               MessageManager.getString("label.chimera_failed"),
321               MessageManager.getString("label.error_loading_file"),
322               JvOptionPane.ERROR_MESSAGE);
323       this.dispose();
324       return;
325     }
326
327     if (this.chimeraSessionFile != null)
328     {
329       boolean opened = jmb.openSession(chimeraSessionFile);
330       if (!opened)
331       {
332         System.err.println("An error occurred opening Chimera session file "
333                 + chimeraSessionFile);
334       }
335     }
336
337     jmb.startChimeraListener();
338   }
339
340   /**
341    * Close down this instance of Jalview's Chimera viewer, giving the user the
342    * option to close the associated Chimera window (process). They may wish to
343    * keep it open until they have had an opportunity to save any work.
344    * 
345    * @param closeChimera
346    *          if true, close any linked Chimera process; if false, prompt first
347    */
348   @Override
349   public void closeViewer(boolean closeChimera)
350   {
351     if (jmb != null && jmb.isChimeraRunning())
352     {
353       if (!closeChimera)
354       {
355         String prompt = MessageManager
356                 .formatMessage("label.confirm_close_chimera", new Object[]
357                 { jmb.getViewerTitle(getViewerName(), false) });
358         prompt = JvSwingUtils.wrapTooltip(true, prompt);
359         int confirm = JvOptionPane.showConfirmDialog(this, prompt,
360                 MessageManager.getString("label.close_viewer"),
361                 JvOptionPane.YES_NO_CANCEL_OPTION);
362         /*
363          * abort closure if user hits escape or Cancel
364          */
365         if (confirm == JvOptionPane.CANCEL_OPTION
366                 || confirm == JvOptionPane.CLOSED_OPTION)
367         {
368           return;
369         }
370         closeChimera = confirm == JvOptionPane.YES_OPTION;
371       }
372       jmb.closeViewer(closeChimera);
373     }
374     setAlignmentPanel(null);
375     _aps.clear();
376     _alignwith.clear();
377     _colourwith.clear();
378     // TODO: check for memory leaks where instance isn't finalised because jmb
379     // holds a reference to the window
380     jmb = null;
381     dispose();
382   }
383
384   /**
385    * Open any newly added PDB structures in Chimera, having first fetched data
386    * from PDB (if not already saved).
387    */
388   @Override
389   public void run()
390   {
391     _started = true;
392     // todo - record which pdbids were successfully imported.
393     StringBuilder errormsgs = new StringBuilder(128);
394     StringBuilder files = new StringBuilder(128);
395     List<PDBEntry> filePDB = new ArrayList<>();
396     List<Integer> filePDBpos = new ArrayList<>();
397     PDBEntry thePdbEntry = null;
398     StructureFile pdb = null;
399     try
400     {
401       String[] curfiles = jmb.getStructureFiles(); // files currently in viewer
402       // TODO: replace with reference fetching/transfer code (validate PDBentry
403       // as a DBRef?)
404       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
405       {
406         String file = null;
407         thePdbEntry = jmb.getPdbEntry(pi);
408         if (thePdbEntry.getFile() == null)
409         {
410           /*
411            * Retrieve PDB data, save to file, attach to PDBEntry
412            */
413           file = fetchPdbFile(thePdbEntry);
414           if (file == null)
415           {
416             errormsgs.append("'" + thePdbEntry.getId() + "' ");
417           }
418         }
419         else
420         {
421           /*
422            * Got file already - ignore if already loaded in Chimera.
423            */
424           file = new File(thePdbEntry.getFile()).getAbsoluteFile()
425                   .getPath();
426           if (curfiles != null && curfiles.length > 0)
427           {
428             addingStructures = true; // already files loaded.
429             for (int c = 0; c < curfiles.length; c++)
430             {
431               if (curfiles[c].equals(file))
432               {
433                 file = null;
434                 break;
435               }
436             }
437           }
438         }
439         if (file != null)
440         {
441           filePDB.add(thePdbEntry);
442           filePDBpos.add(Integer.valueOf(pi));
443           files.append(" \"" + Platform.escapeBackslashes(file) + "\"");
444         }
445       }
446     } catch (OutOfMemoryError oomerror)
447     {
448       new OOMWarning("Retrieving PDB files: " + thePdbEntry.getId(),
449               oomerror);
450     } catch (Exception ex)
451     {
452       ex.printStackTrace();
453       errormsgs.append(
454               "When retrieving pdbfiles for '" + thePdbEntry.getId() + "'");
455     }
456     if (errormsgs.length() > 0)
457     {
458
459       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
460               MessageManager.formatMessage(
461                       "label.pdb_entries_couldnt_be_retrieved", new Object[]
462                       { errormsgs.toString() }),
463               MessageManager.getString("label.couldnt_load_file"),
464               JvOptionPane.ERROR_MESSAGE);
465     }
466
467     if (files.length() > 0)
468     {
469       jmb.setFinishedInit(false);
470       if (!addingStructures)
471       {
472         try
473         {
474           initChimera();
475         } catch (Exception ex)
476         {
477           Cache.log.error("Couldn't open Chimera viewer!", ex);
478         }
479       }
480       int num = -1;
481       for (PDBEntry pe : filePDB)
482       {
483         num++;
484         if (pe.getFile() != null)
485         {
486           try
487           {
488             int pos = filePDBpos.get(num).intValue();
489             long startTime = startProgressBar(getViewerName() + " "
490                     + MessageManager.getString("status.opening_file_for")
491                     + " " + pe.getId());
492             jmb.openFile(pe);
493             jmb.addSequence(pos, jmb.getSequence()[pos]);
494             File fl = new File(pe.getFile());
495             DataSourceType protocol = DataSourceType.URL;
496             try
497             {
498               if (fl.exists())
499               {
500                 protocol = DataSourceType.FILE;
501               }
502             } catch (Throwable e)
503             {
504             } finally
505             {
506               stopProgressBar("", startTime);
507             }
508             // Explicitly map to the filename used by Chimera ;
509
510             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
511                     jmb.getChains()[pos], pe.getFile(), protocol,
512                     getProgressIndicator());
513             stashFoundChains(pdb, pe.getFile());
514
515           } catch (OutOfMemoryError oomerror)
516           {
517             new OOMWarning(
518                     "When trying to open and map structures from Chimera!",
519                     oomerror);
520           } catch (Exception ex)
521           {
522             Cache.log.error(
523                     "Couldn't open " + pe.getFile() + " in Chimera viewer!",
524                     ex);
525           } finally
526           {
527             Cache.log.debug("File locations are " + files);
528           }
529         }
530       }
531
532       jmb.refreshGUI();
533       jmb.setFinishedInit(true);
534       jmb.setLoadingFromArchive(false);
535
536       /*
537        * ensure that any newly discovered features (e.g. RESNUM)
538        * are added to any open feature settings dialog
539        */
540       FeatureRenderer fr = getBinding().getFeatureRenderer(null);
541       if (fr != null)
542       {
543         fr.featuresAdded();
544       }
545
546       // refresh the sequence colours for the new structure(s)
547       for (AlignmentViewPanel ap : _colourwith)
548       {
549         jmb.updateColours(ap);
550       }
551       // do superposition if asked to
552       if (alignAddedStructures)
553       {
554         new Thread(new Runnable()
555         {
556           @Override
557           public void run()
558           {
559             alignStructsWithAllAlignPanels();
560           }
561         }).start();
562       }
563       addingStructures = false;
564     }
565     _started = false;
566     worker = null;
567   }
568
569   /**
570    * Fetch PDB data and save to a local file. Returns the full path to the file,
571    * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
572    * 
573    * @param processingEntry
574    * @return
575    * @throws Exception
576    */
577
578   private void stashFoundChains(StructureFile pdb, String file)
579   {
580     for (int i = 0; i < pdb.getChains().size(); i++)
581     {
582       String chid = new String(
583               pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
584       jmb.getChainNames().add(chid);
585       jmb.addChainFile(chid, file);
586     }
587   }
588
589   private String fetchPdbFile(PDBEntry processingEntry) throws Exception
590   {
591     String filePath = null;
592     Pdb pdbclient = new Pdb();
593     AlignmentI pdbseq = null;
594     String pdbid = processingEntry.getId();
595     long handle = System.currentTimeMillis()
596             + Thread.currentThread().hashCode();
597
598     /*
599      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
600      */
601     String msg = MessageManager.formatMessage("status.fetching_pdb",
602             new Object[]
603             { pdbid });
604     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
605     // long hdl = startProgressBar(MessageManager.formatMessage(
606     // "status.fetching_pdb", new Object[]
607     // { pdbid }));
608     try
609     {
610       pdbseq = pdbclient.getSequenceRecords(pdbid);
611     } catch (OutOfMemoryError oomerror)
612     {
613       new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
614     } finally
615     {
616       msg = pdbid + " " + MessageManager.getString("label.state_completed");
617       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
618       // stopProgressBar(msg, hdl);
619     }
620     /*
621      * If PDB data were saved and are not invalid (empty alignment), return the
622      * file path.
623      */
624     if (pdbseq != null && pdbseq.getHeight() > 0)
625     {
626       // just use the file name from the first sequence's first PDBEntry
627       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
628               .elementAt(0).getFile()).getAbsolutePath();
629       processingEntry.setFile(filePath);
630     }
631     return filePath;
632   }
633
634   @Override
635   public void eps_actionPerformed()
636   {
637     throw new Error(MessageManager
638             .getString("error.eps_generation_not_implemented"));
639   }
640
641   @Override
642   public void png_actionPerformed()
643   {
644     throw new Error(MessageManager
645             .getString("error.png_generation_not_implemented"));
646   }
647
648   @Override
649   public void showHelp_actionPerformed()
650   {
651     try
652     {
653       String url = jmb.getHelpURL();
654       BrowserLauncher.openURL(url);
655     } catch (IOException ex)
656     {
657       System.err
658               .println("Show Chimera help failed with: " + ex.getMessage());
659     }
660   }
661
662   @Override
663   public AAStructureBindingModel getBinding()
664   {
665     return jmb;
666   }
667
668   /**
669    * Ask Chimera to save its session to the designated file path, or to a
670    * temporary file if the path is null. Returns the file path if successful,
671    * else null.
672    * 
673    * @param filepath
674    * @see getStateInfo
675    */
676   protected String saveSession(String filepath)
677   {
678     String pathUsed = filepath;
679     try
680     {
681       if (pathUsed == null)
682       {
683         String suffix = jmb.getSessionFileExtension();
684         File tempFile = File.createTempFile("chimera", suffix);
685         tempFile.deleteOnExit();
686         pathUsed = tempFile.getPath();
687       }
688       boolean result = jmb.saveSession(pathUsed);
689       if (result)
690       {
691         this.chimeraSessionFile = pathUsed;
692         return pathUsed;
693       }
694     } catch (IOException e)
695     {
696     }
697     return null;
698   }
699
700   /**
701    * Returns a string representing the state of the Chimera session. This is
702    * done by requesting Chimera to save its session to a temporary file, then
703    * reading the file contents. Returns an empty string on any error.
704    */
705   @Override
706   public String getStateInfo()
707   {
708     String sessionFile = saveSession(null);
709     if (sessionFile == null)
710     {
711       return "";
712     }
713     InputStream is = null;
714     try
715     {
716       File f = new File(sessionFile);
717       byte[] bytes = new byte[(int) f.length()];
718       is = new FileInputStream(sessionFile);
719       is.read(bytes);
720       return new String(bytes);
721     } catch (IOException e)
722     {
723       return "";
724     } finally
725     {
726       if (is != null)
727       {
728         try
729         {
730           is.close();
731         } catch (IOException e)
732         {
733           // ignore
734         }
735       }
736     }
737   }
738
739   @Override
740   protected void fitToWindow_actionPerformed()
741   {
742     jmb.focusView();
743   }
744
745   @Override
746   public ViewerType getViewerType()
747   {
748     return ViewerType.CHIMERA;
749   }
750
751   @Override
752   protected String getViewerName()
753   {
754     return "Chimera";
755   }
756
757   /**
758    * Sends commands to align structures according to associated alignment(s).
759    * 
760    * @return
761    */
762   @Override
763   protected String alignStructsWithAllAlignPanels()
764   {
765     String reply = super.alignStructsWithAllAlignPanels();
766     if (reply != null)
767     {
768       statusBar.setText("Superposition failed: " + reply);
769     }
770     return reply;
771   }
772 }