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