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