JAL-3518 basic refactoring / pull up of superposeStructures; to tidy!
[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 jalview.api.AlignmentViewPanel;
24 import jalview.api.FeatureRenderer;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.PDBEntry;
28 import jalview.datamodel.SequenceI;
29 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
30 import jalview.gui.StructureViewer.ViewerType;
31 import jalview.io.DataSourceType;
32 import jalview.io.StructureFile;
33 import jalview.structures.models.AAStructureBindingModel;
34 import jalview.util.BrowserLauncher;
35 import jalview.util.MessageManager;
36 import jalview.util.Platform;
37 import jalview.ws.dbsources.Pdb;
38
39 import java.awt.event.ActionEvent;
40 import java.awt.event.ActionListener;
41 import java.awt.event.MouseAdapter;
42 import java.awt.event.MouseEvent;
43 import java.io.File;
44 import java.io.FileInputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.List;
50
51 import javax.swing.JInternalFrame;
52 import javax.swing.JMenu;
53 import javax.swing.JMenuItem;
54 import javax.swing.event.InternalFrameAdapter;
55 import javax.swing.event.InternalFrameEvent;
56
57 /**
58  * GUI elements for handling an external chimera display
59  * 
60  * @author jprocter
61  *
62  */
63 public class ChimeraViewFrame extends StructureViewerBase
64 {
65   private JalviewChimeraBinding jmb;
66
67   /*
68    * Path to Chimera session file. This is set when an open Jalview/Chimera
69    * session is saved, or on restore from a Jalview project (if it holds the
70    * filename of any saved Chimera sessions).
71    */
72   private String chimeraSessionFile = null;
73
74   private int myWidth = 500;
75
76   private int myHeight = 150;
77
78   /**
79    * Initialise menu options.
80    */
81   @Override
82   protected void initMenus()
83   {
84     super.initMenus();
85
86     viewerActionMenu.setText(MessageManager.getString("label.chimera"));
87
88     viewerColour
89             .setText(MessageManager.getString("label.colour_with_chimera"));
90     viewerColour.setToolTipText(MessageManager
91             .getString("label.let_chimera_manage_structure_colours"));
92
93     helpItem.setText(MessageManager.getString("label.chimera_help"));
94     savemenu.setVisible(false); // not yet implemented
95     viewMenu.add(fitToWindow);
96
97     JMenuItem writeFeatures = new JMenuItem(
98             MessageManager.getString("label.create_chimera_attributes"));
99     writeFeatures.setToolTipText(MessageManager
100             .getString("label.create_chimera_attributes_tip"));
101     writeFeatures.addActionListener(new ActionListener()
102     {
103       @Override
104       public void actionPerformed(ActionEvent e)
105       {
106         sendFeaturesToChimera();
107       }
108     });
109     viewerActionMenu.add(writeFeatures);
110
111     final JMenu fetchAttributes = new JMenu(
112             MessageManager.getString("label.fetch_chimera_attributes"));
113     fetchAttributes.setToolTipText(
114             MessageManager.getString("label.fetch_chimera_attributes_tip"));
115     fetchAttributes.addMouseListener(new MouseAdapter()
116     {
117
118       @Override
119       public void mouseEntered(MouseEvent e)
120       {
121         buildAttributesMenu(fetchAttributes);
122       }
123     });
124     viewerActionMenu.add(fetchAttributes);
125   }
126
127   /**
128    * Query Chimera for its residue attribute names and add them as items off the
129    * attributes menu
130    * 
131    * @param attributesMenu
132    */
133   protected void buildAttributesMenu(JMenu attributesMenu)
134   {
135     List<String> atts = jmb.getChimeraAttributes();
136     attributesMenu.removeAll();
137     Collections.sort(atts);
138     for (String attName : atts)
139     {
140       JMenuItem menuItem = new JMenuItem(attName);
141       menuItem.addActionListener(new ActionListener()
142       {
143         @Override
144         public void actionPerformed(ActionEvent e)
145         {
146           getChimeraAttributes(attName);
147         }
148       });
149       attributesMenu.add(menuItem);
150     }
151   }
152
153   /**
154    * Read residues in Chimera with the given attribute name, and set as features
155    * on the corresponding sequence positions (if any)
156    * 
157    * @param attName
158    */
159   protected void getChimeraAttributes(String attName)
160   {
161     jmb.copyStructureAttributesToFeatures(attName, getAlignmentPanel());
162   }
163
164   /**
165    * Send a command to Chimera to create residue attributes for Jalview features
166    * <p>
167    * The syntax is: setattr r &lt;attName&gt; &lt;attValue&gt; &lt;atomSpec&gt;
168    * <p>
169    * For example: setattr r jv_chain "Ferredoxin-1, Chloroplastic" #0:94.A
170    */
171   protected void sendFeaturesToChimera()
172   {
173     int count = jmb.sendFeaturesToViewer(getAlignmentPanel());
174     statusBar.setText(
175             MessageManager.formatMessage("label.attributes_set", count));
176   }
177
178   /**
179    * open a single PDB structure in a new Chimera view
180    * 
181    * @param pdbentry
182    * @param seq
183    * @param chains
184    * @param ap
185    */
186   public ChimeraViewFrame(PDBEntry pdbentry, SequenceI[] seq,
187           String[] chains, final AlignmentPanel ap)
188   {
189     this();
190
191     openNewChimera(ap, new PDBEntry[] { pdbentry },
192             new SequenceI[][]
193             { seq });
194   }
195
196   /**
197    * Create a helper to manage progress bar display
198    */
199   protected void createProgressBar()
200   {
201     if (getProgressIndicator() == null)
202     {
203       setProgressIndicator(new ProgressBar(statusPanel, statusBar));
204     }
205   }
206
207   private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
208           SequenceI[][] seqs)
209   {
210     createProgressBar();
211     jmb = newBindingModel(ap, pdbentrys, seqs);
212     addAlignmentPanel(ap);
213     useAlignmentPanelForColourbyseq(ap);
214
215     if (pdbentrys.length > 1)
216     {
217       useAlignmentPanelForSuperposition(ap);
218     }
219     jmb.setColourBySequence(true);
220     setSize(myWidth, myHeight);
221     initMenus();
222
223     addingStructures = false;
224     worker = new Thread(this);
225     worker.start();
226
227     this.addInternalFrameListener(new InternalFrameAdapter()
228     {
229       @Override
230       public void internalFrameClosing(
231               InternalFrameEvent internalFrameEvent)
232       {
233         closeViewer(false);
234       }
235     });
236
237   }
238
239   protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
240           PDBEntry[] pdbentrys, SequenceI[][] seqs)
241   {
242     return new JalviewChimeraBindingModel(this,
243             ap.getStructureSelectionManager(), pdbentrys, seqs, null);
244   }
245
246   /**
247    * Create a new viewer from saved session state data including Chimera session
248    * file
249    * 
250    * @param chimeraSessionFile
251    * @param alignPanel
252    * @param pdbArray
253    * @param seqsArray
254    * @param colourByChimera
255    * @param colourBySequence
256    * @param newViewId
257    */
258   public ChimeraViewFrame(String chimeraSessionFile,
259           AlignmentPanel alignPanel, PDBEntry[] pdbArray,
260           SequenceI[][] seqsArray, boolean colourByChimera,
261           boolean colourBySequence, String newViewId)
262   {
263     this();
264     setViewId(newViewId);
265     this.chimeraSessionFile = chimeraSessionFile;
266     openNewChimera(alignPanel, pdbArray, seqsArray);
267     if (colourByChimera)
268     {
269       jmb.setColourBySequence(false);
270       seqColour.setSelected(false);
271       viewerColour.setSelected(true);
272     }
273     else if (colourBySequence)
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.getString("label.chimera_failed"),
327               MessageManager.getString("label.error_loading_file"),
328               JvOptionPane.ERROR_MESSAGE);
329       this.dispose();
330       return;
331     }
332
333     if (this.chimeraSessionFile != null)
334     {
335       boolean opened = jmb.openSession(chimeraSessionFile);
336       if (!opened)
337       {
338         System.err.println("An error occurred opening Chimera session file "
339                 + chimeraSessionFile);
340       }
341     }
342
343     jmb.startChimeraListener();
344   }
345
346   /**
347    * Close down this instance of Jalview's Chimera viewer, giving the user the
348    * option to close the associated Chimera window (process). They may wish to
349    * keep it open until they have had an opportunity to save any work.
350    * 
351    * @param closeChimera
352    *          if true, close any linked Chimera process; if false, prompt first
353    */
354   @Override
355   public void closeViewer(boolean closeChimera)
356   {
357     if (jmb != null && jmb.isChimeraRunning())
358     {
359       if (!closeChimera)
360       {
361         String prompt = MessageManager
362                 .formatMessage("label.confirm_close_chimera", new Object[]
363                 { jmb.getViewerTitle(getViewerName(), false) });
364         prompt = JvSwingUtils.wrapTooltip(true, prompt);
365         int confirm = JvOptionPane.showConfirmDialog(this, prompt,
366                 MessageManager.getString("label.close_viewer"),
367                 JvOptionPane.YES_NO_CANCEL_OPTION);
368         /*
369          * abort closure if user hits escape or Cancel
370          */
371         if (confirm == JvOptionPane.CANCEL_OPTION
372                 || confirm == JvOptionPane.CLOSED_OPTION)
373         {
374           return;
375         }
376         closeChimera = confirm == JvOptionPane.YES_OPTION;
377       }
378       jmb.closeViewer(closeChimera);
379     }
380     setAlignmentPanel(null);
381     _aps.clear();
382     _alignwith.clear();
383     _colourwith.clear();
384     // TODO: check for memory leaks where instance isn't finalised because jmb
385     // holds a reference to the window
386     jmb = null;
387     dispose();
388   }
389
390   /**
391    * Open any newly added PDB structures in Chimera, having first fetched data
392    * from PDB (if not already saved).
393    */
394   @Override
395   public void run()
396   {
397     _started = true;
398     // todo - record which pdbids were successfully imported.
399     StringBuilder errormsgs = new StringBuilder(128);
400     StringBuilder files = new StringBuilder(128);
401     List<PDBEntry> filePDB = new ArrayList<>();
402     List<Integer> filePDBpos = new ArrayList<>();
403     PDBEntry thePdbEntry = null;
404     StructureFile pdb = null;
405     try
406     {
407       String[] curfiles = jmb.getStructureFiles(); // files currently in viewer
408       // TODO: replace with reference fetching/transfer code (validate PDBentry
409       // as a DBRef?)
410       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
411       {
412         String file = null;
413         thePdbEntry = jmb.getPdbEntry(pi);
414         if (thePdbEntry.getFile() == null)
415         {
416           /*
417            * Retrieve PDB data, save to file, attach to PDBEntry
418            */
419           file = fetchPdbFile(thePdbEntry);
420           if (file == null)
421           {
422             errormsgs.append("'" + thePdbEntry.getId() + "' ");
423           }
424         }
425         else
426         {
427           /*
428            * Got file already - ignore if already loaded in Chimera.
429            */
430           file = new File(thePdbEntry.getFile()).getAbsoluteFile()
431                   .getPath();
432           if (curfiles != null && curfiles.length > 0)
433           {
434             addingStructures = true; // already files loaded.
435             for (int c = 0; c < curfiles.length; c++)
436             {
437               if (curfiles[c].equals(file))
438               {
439                 file = null;
440                 break;
441               }
442             }
443           }
444         }
445         if (file != null)
446         {
447           filePDB.add(thePdbEntry);
448           filePDBpos.add(Integer.valueOf(pi));
449           files.append(" \"" + Platform.escapeBackslashes(file) + "\"");
450         }
451       }
452     } catch (OutOfMemoryError oomerror)
453     {
454       new OOMWarning("Retrieving PDB files: " + thePdbEntry.getId(),
455               oomerror);
456     } catch (Exception ex)
457     {
458       ex.printStackTrace();
459       errormsgs.append(
460               "When retrieving pdbfiles for '" + thePdbEntry.getId() + "'");
461     }
462     if (errormsgs.length() > 0)
463     {
464
465       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
466               MessageManager.formatMessage(
467                       "label.pdb_entries_couldnt_be_retrieved", new Object[]
468                       { errormsgs.toString() }),
469               MessageManager.getString("label.couldnt_load_file"),
470               JvOptionPane.ERROR_MESSAGE);
471     }
472
473     if (files.length() > 0)
474     {
475       jmb.setFinishedInit(false);
476       if (!addingStructures)
477       {
478         try
479         {
480           initChimera();
481         } catch (Exception ex)
482         {
483           Cache.log.error("Couldn't open Chimera viewer!", ex);
484         }
485       }
486       int num = -1;
487       for (PDBEntry pe : filePDB)
488       {
489         num++;
490         if (pe.getFile() != null)
491         {
492           try
493           {
494             int pos = filePDBpos.get(num).intValue();
495             long startTime = startProgressBar(getViewerName() + " "
496                     + MessageManager.getString("status.opening_file_for")
497                     + " " + pe.getId());
498             jmb.openFile(pe);
499             jmb.addSequence(pos, jmb.getSequence()[pos]);
500             File fl = new File(pe.getFile());
501             DataSourceType protocol = DataSourceType.URL;
502             try
503             {
504               if (fl.exists())
505               {
506                 protocol = DataSourceType.FILE;
507               }
508             } catch (Throwable e)
509             {
510             } finally
511             {
512               stopProgressBar("", startTime);
513             }
514             // Explicitly map to the filename used by Chimera ;
515
516             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
517                     jmb.getChains()[pos], pe.getFile(), protocol,
518                     getProgressIndicator());
519             stashFoundChains(pdb, pe.getFile());
520
521           } catch (OutOfMemoryError oomerror)
522           {
523             new OOMWarning(
524                     "When trying to open and map structures from Chimera!",
525                     oomerror);
526           } catch (Exception ex)
527           {
528             Cache.log.error(
529                     "Couldn't open " + pe.getFile() + " in Chimera viewer!",
530                     ex);
531           } finally
532           {
533             Cache.log.debug("File locations are " + files);
534           }
535         }
536       }
537
538       jmb.refreshGUI();
539       jmb.setFinishedInit(true);
540       jmb.setLoadingFromArchive(false);
541
542       /*
543        * ensure that any newly discovered features (e.g. RESNUM)
544        * are added to any open feature settings dialog
545        */
546       FeatureRenderer fr = getBinding().getFeatureRenderer(null);
547       if (fr != null)
548       {
549         fr.featuresAdded();
550       }
551
552       // refresh the sequence colours for the new structure(s)
553       for (AlignmentViewPanel ap : _colourwith)
554       {
555         jmb.updateColours(ap);
556       }
557       // do superposition if asked to
558       if (alignAddedStructures)
559       {
560         new Thread(new Runnable()
561         {
562           @Override
563           public void run()
564           {
565             alignStructsWithAllAlignPanels();
566           }
567         }).start();
568       }
569       addingStructures = false;
570     }
571     _started = false;
572     worker = null;
573   }
574
575   /**
576    * Fetch PDB data and save to a local file. Returns the full path to the file,
577    * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
578    * 
579    * @param processingEntry
580    * @return
581    * @throws Exception
582    */
583
584   private void stashFoundChains(StructureFile pdb, String file)
585   {
586     for (int i = 0; i < pdb.getChains().size(); i++)
587     {
588       String chid = new String(
589               pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
590       jmb.getChainNames().add(chid);
591       jmb.addChainFile(chid, file);
592     }
593   }
594
595   private String fetchPdbFile(PDBEntry processingEntry) throws Exception
596   {
597     String filePath = null;
598     Pdb pdbclient = new Pdb();
599     AlignmentI pdbseq = null;
600     String pdbid = processingEntry.getId();
601     long handle = System.currentTimeMillis()
602             + Thread.currentThread().hashCode();
603
604     /*
605      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
606      */
607     String msg = MessageManager.formatMessage("status.fetching_pdb",
608             new Object[]
609             { pdbid });
610     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
611     // long hdl = startProgressBar(MessageManager.formatMessage(
612     // "status.fetching_pdb", new Object[]
613     // { pdbid }));
614     try
615     {
616       pdbseq = pdbclient.getSequenceRecords(pdbid);
617     } catch (OutOfMemoryError oomerror)
618     {
619       new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
620     } finally
621     {
622       msg = pdbid + " " + MessageManager.getString("label.state_completed");
623       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
624       // stopProgressBar(msg, hdl);
625     }
626     /*
627      * If PDB data were saved and are not invalid (empty alignment), return the
628      * file path.
629      */
630     if (pdbseq != null && pdbseq.getHeight() > 0)
631     {
632       // just use the file name from the first sequence's first PDBEntry
633       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
634               .elementAt(0).getFile()).getAbsolutePath();
635       processingEntry.setFile(filePath);
636     }
637     return filePath;
638   }
639
640   @Override
641   public void eps_actionPerformed()
642   {
643     throw new Error(MessageManager
644             .getString("error.eps_generation_not_implemented"));
645   }
646
647   @Override
648   public void png_actionPerformed()
649   {
650     throw new Error(MessageManager
651             .getString("error.png_generation_not_implemented"));
652   }
653
654   @Override
655   public void showHelp_actionPerformed()
656   {
657     try
658     {
659       String url = jmb.getHelpURL();
660       BrowserLauncher.openURL(url);
661     } catch (IOException ex)
662     {
663       System.err
664               .println("Show Chimera help failed with: " + ex.getMessage());
665     }
666   }
667
668   @Override
669   public AAStructureBindingModel getBinding()
670   {
671     return jmb;
672   }
673
674   /**
675    * Ask Chimera to save its session to the designated file path, or to a
676    * temporary file if the path is null. Returns the file path if successful,
677    * else null.
678    * 
679    * @param filepath
680    * @see getStateInfo
681    */
682   protected String saveSession(String filepath)
683   {
684     String pathUsed = filepath;
685     try
686     {
687       if (pathUsed == null)
688       {
689         String suffix = jmb.getSessionFileExtension();
690         File tempFile = File.createTempFile("chimera", suffix);
691         tempFile.deleteOnExit();
692         pathUsed = tempFile.getPath();
693       }
694       boolean result = jmb.saveSession(pathUsed);
695       if (result)
696       {
697         this.chimeraSessionFile = pathUsed;
698         return pathUsed;
699       }
700     } catch (IOException e)
701     {
702     }
703     return null;
704   }
705
706   /**
707    * Returns a string representing the state of the Chimera session. This is
708    * done by requesting Chimera to save its session to a temporary file, then
709    * reading the file contents. Returns an empty string on any error.
710    */
711   @Override
712   public String getStateInfo()
713   {
714     String sessionFile = saveSession(null);
715     if (sessionFile == null)
716     {
717       return "";
718     }
719     InputStream is = null;
720     try
721     {
722       File f = new File(sessionFile);
723       byte[] bytes = new byte[(int) f.length()];
724       is = new FileInputStream(sessionFile);
725       is.read(bytes);
726       return new String(bytes);
727     } catch (IOException e)
728     {
729       return "";
730     } finally
731     {
732       if (is != null)
733       {
734         try
735         {
736           is.close();
737         } catch (IOException e)
738         {
739           // ignore
740         }
741       }
742     }
743   }
744
745   @Override
746   protected void fitToWindow_actionPerformed()
747   {
748     jmb.focusView();
749   }
750
751   @Override
752   public ViewerType getViewerType()
753   {
754     return ViewerType.CHIMERA;
755   }
756
757   @Override
758   protected String getViewerName()
759   {
760     return "Chimera";
761   }
762 }