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