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