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