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