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