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