JAL-2422 pull-up refactoring of structure commands (continued)
[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 <attName> <attValue> <atomSpec>
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             alignStructs_withAllAlignPanels();
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(ActionEvent e)
636   {
637     throw new Error(MessageManager
638             .getString("error.eps_generation_not_implemented"));
639   }
640
641   @Override
642   public void png_actionPerformed(ActionEvent e)
643   {
644     throw new Error(MessageManager
645             .getString("error.png_generation_not_implemented"));
646   }
647
648   @Override
649   public void showHelp_actionPerformed(ActionEvent actionEvent)
650   {
651     try
652     {
653       String url = jmb.isChimeraX()
654               ? "http://www.rbvi.ucsf.edu/chimerax/docs/user/index.html"
655               : "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide";
656       BrowserLauncher.openURL(url);
657     } catch (IOException ex)
658     {
659       System.err
660               .println("Show Chimera help failed with: " + ex.getMessage());
661     }
662   }
663
664   @Override
665   public AAStructureBindingModel getBinding()
666   {
667     return jmb;
668   }
669
670   /**
671    * Ask Chimera to save its session to the designated file path, or to a
672    * temporary file if the path is null. Returns the file path if successful,
673    * else null.
674    * 
675    * @param filepath
676    * @see getStateInfo
677    */
678   protected String saveSession(String filepath)
679   {
680     String pathUsed = filepath;
681     try
682     {
683       if (pathUsed == null)
684       {
685         String suffix = jmb.isChimeraX() ? ".cxs" : ".py";
686         File tempFile = File.createTempFile("chimera", suffix);
687         tempFile.deleteOnExit();
688         pathUsed = tempFile.getPath();
689       }
690       boolean result = jmb.saveSession(pathUsed);
691       if (result)
692       {
693         this.chimeraSessionFile = pathUsed;
694         return pathUsed;
695       }
696     } catch (IOException e)
697     {
698     }
699     return null;
700   }
701
702   /**
703    * Returns a string representing the state of the Chimera session. This is
704    * done by requesting Chimera to save its session to a temporary file, then
705    * reading the file contents. Returns an empty string on any error.
706    */
707   @Override
708   public String getStateInfo()
709   {
710     String sessionFile = saveSession(null);
711     if (sessionFile == null)
712     {
713       return "";
714     }
715     InputStream is = null;
716     try
717     {
718       File f = new File(sessionFile);
719       byte[] bytes = new byte[(int) f.length()];
720       is = new FileInputStream(sessionFile);
721       is.read(bytes);
722       return new String(bytes);
723     } catch (IOException e)
724     {
725       return "";
726     } finally
727     {
728       if (is != null)
729       {
730         try
731         {
732           is.close();
733         } catch (IOException e)
734         {
735           // ignore
736         }
737       }
738     }
739   }
740
741   @Override
742   protected void fitToWindow_actionPerformed()
743   {
744     jmb.focusView();
745   }
746
747   @Override
748   public ViewerType getViewerType()
749   {
750     return ViewerType.CHIMERA;
751   }
752
753   @Override
754   protected String getViewerName()
755   {
756     return "Chimera";
757   }
758
759   /**
760    * Sends commands to align structures according to associated alignment(s).
761    * 
762    * @return
763    */
764   @Override
765   protected String alignStructs_withAllAlignPanels()
766   {
767     String reply = super.alignStructs_withAllAlignPanels();
768     if (reply != null)
769     {
770       statusBar.setText("Superposition failed: " + reply);
771     }
772     return reply;
773   }
774 }