JAL-2422 JAL-3551 unit tests updated for code changes
[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.formatMessage("label.open_viewer_failed",
318                       getViewerName()),
319               MessageManager.getString("label.error_loading_file"),
320               JvOptionPane.ERROR_MESSAGE);
321       this.dispose();
322       return;
323     }
324
325     if (this.chimeraSessionFile != null)
326     {
327       boolean opened = jmb.openSession(chimeraSessionFile);
328       if (!opened)
329       {
330         System.err.println("An error occurred opening Chimera session file "
331                 + chimeraSessionFile);
332       }
333     }
334
335     jmb.startChimeraListener();
336   }
337
338   /**
339    * Open any newly added PDB structures in Chimera, having first fetched data
340    * from PDB (if not already saved).
341    */
342   @Override
343   public void run()
344   {
345     _started = true;
346     // todo - record which pdbids were successfully imported.
347     StringBuilder errormsgs = new StringBuilder(128);
348     StringBuilder files = new StringBuilder(128);
349     List<PDBEntry> filePDB = new ArrayList<>();
350     List<Integer> filePDBpos = new ArrayList<>();
351     PDBEntry thePdbEntry = null;
352     StructureFile pdb = null;
353     try
354     {
355       String[] curfiles = jmb.getStructureFiles(); // files currently in viewer
356       // TODO: replace with reference fetching/transfer code (validate PDBentry
357       // as a DBRef?)
358       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
359       {
360         String file = null;
361         thePdbEntry = jmb.getPdbEntry(pi);
362         if (thePdbEntry.getFile() == null)
363         {
364           /*
365            * Retrieve PDB data, save to file, attach to PDBEntry
366            */
367           file = fetchPdbFile(thePdbEntry);
368           if (file == null)
369           {
370             errormsgs.append("'" + thePdbEntry.getId() + "' ");
371           }
372         }
373         else
374         {
375           /*
376            * Got file already - ignore if already loaded in Chimera.
377            */
378           file = new File(thePdbEntry.getFile()).getAbsoluteFile()
379                   .getPath();
380           if (curfiles != null && curfiles.length > 0)
381           {
382             addingStructures = true; // already files loaded.
383             for (int c = 0; c < curfiles.length; c++)
384             {
385               if (curfiles[c].equals(file))
386               {
387                 file = null;
388                 break;
389               }
390             }
391           }
392         }
393         if (file != null)
394         {
395           filePDB.add(thePdbEntry);
396           filePDBpos.add(Integer.valueOf(pi));
397           files.append(" \"" + Platform.escapeBackslashes(file) + "\"");
398         }
399       }
400     } catch (OutOfMemoryError oomerror)
401     {
402       new OOMWarning("Retrieving PDB files: " + thePdbEntry.getId(),
403               oomerror);
404     } catch (Exception ex)
405     {
406       ex.printStackTrace();
407       errormsgs.append(
408               "When retrieving pdbfiles for '" + thePdbEntry.getId() + "'");
409     }
410     if (errormsgs.length() > 0)
411     {
412
413       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
414               MessageManager.formatMessage(
415                       "label.pdb_entries_couldnt_be_retrieved", new Object[]
416                       { errormsgs.toString() }),
417               MessageManager.getString("label.couldnt_load_file"),
418               JvOptionPane.ERROR_MESSAGE);
419     }
420
421     if (files.length() > 0)
422     {
423       jmb.setFinishedInit(false);
424       if (!addingStructures)
425       {
426         try
427         {
428           initChimera();
429         } catch (Exception ex)
430         {
431           Cache.log.error("Couldn't open Chimera viewer!", ex);
432         }
433       }
434       int num = -1;
435       for (PDBEntry pe : filePDB)
436       {
437         num++;
438         if (pe.getFile() != null)
439         {
440           try
441           {
442             int pos = filePDBpos.get(num).intValue();
443             long startTime = startProgressBar(getViewerName() + " "
444                     + MessageManager.getString("status.opening_file_for")
445                     + " " + pe.getId());
446             jmb.openFile(pe);
447             jmb.addSequence(pos, jmb.getSequence()[pos]);
448             File fl = new File(pe.getFile());
449             DataSourceType protocol = DataSourceType.URL;
450             try
451             {
452               if (fl.exists())
453               {
454                 protocol = DataSourceType.FILE;
455               }
456             } catch (Throwable e)
457             {
458             } finally
459             {
460               stopProgressBar("", startTime);
461             }
462             // Explicitly map to the filename used by Chimera ;
463
464             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
465                     jmb.getChains()[pos], pe.getFile(), protocol,
466                     getProgressIndicator());
467             jmb.stashFoundChains(pdb, pe.getFile());
468
469           } catch (OutOfMemoryError oomerror)
470           {
471             new OOMWarning(
472                     "When trying to open and map structures from Chimera!",
473                     oomerror);
474           } catch (Exception ex)
475           {
476             Cache.log.error(
477                     "Couldn't open " + pe.getFile() + " in Chimera viewer!",
478                     ex);
479           } finally
480           {
481             Cache.log.debug("File locations are " + files);
482           }
483         }
484       }
485
486       jmb.refreshGUI();
487       jmb.setFinishedInit(true);
488       jmb.setLoadingFromArchive(false);
489
490       /*
491        * ensure that any newly discovered features (e.g. RESNUM)
492        * are added to any open feature settings dialog
493        */
494       FeatureRenderer fr = getBinding().getFeatureRenderer(null);
495       if (fr != null)
496       {
497         fr.featuresAdded();
498       }
499
500       // refresh the sequence colours for the new structure(s)
501       for (AlignmentViewPanel ap : _colourwith)
502       {
503         jmb.updateColours(ap);
504       }
505       // do superposition if asked to
506       if (alignAddedStructures)
507       {
508         new Thread(new Runnable()
509         {
510           @Override
511           public void run()
512           {
513             alignStructsWithAllAlignPanels();
514           }
515         }).start();
516       }
517       addingStructures = false;
518     }
519     _started = false;
520     worker = null;
521   }
522
523   @Override
524   public void eps_actionPerformed()
525   {
526     throw new Error(MessageManager
527             .getString("error.eps_generation_not_implemented"));
528   }
529
530   @Override
531   public void png_actionPerformed()
532   {
533     throw new Error(MessageManager
534             .getString("error.png_generation_not_implemented"));
535   }
536
537   @Override
538   public void showHelp_actionPerformed()
539   {
540     try
541     {
542       String url = jmb.getHelpURL();
543       BrowserLauncher.openURL(url);
544     } catch (IOException ex)
545     {
546       System.err
547               .println("Show Chimera help failed with: " + ex.getMessage());
548     }
549   }
550
551   @Override
552   public AAStructureBindingModel getBinding()
553   {
554     return jmb;
555   }
556
557   @Override
558   protected void fitToWindow_actionPerformed()
559   {
560     jmb.focusView();
561   }
562
563   @Override
564   public ViewerType getViewerType()
565   {
566     return ViewerType.CHIMERA;
567   }
568
569   @Override
570   protected String getViewerName()
571   {
572     return "Chimera";
573   }
574 }