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