JAL-2738 copy to spikes/mungo
[jalview.git] / src / jalview / gui / AppJmol.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.gui.StructureViewer.ViewerType;
28 import jalview.structures.models.AAStructureBindingModel;
29 import jalview.util.BrowserLauncher;
30 import jalview.util.MessageManager;
31 import jalview.util.Platform;
32 import jalview.ws.dbsources.Pdb;
33
34 import java.awt.BorderLayout;
35 import java.awt.Color;
36 import java.awt.Dimension;
37 import java.awt.Font;
38 import java.awt.Graphics;
39 import java.awt.Rectangle;
40 import java.awt.event.ActionEvent;
41 import java.io.File;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Vector;
45
46 import javax.swing.JCheckBoxMenuItem;
47 import javax.swing.JInternalFrame;
48 import javax.swing.JPanel;
49 import javax.swing.JSplitPane;
50 import javax.swing.SwingUtilities;
51 import javax.swing.event.InternalFrameAdapter;
52 import javax.swing.event.InternalFrameEvent;
53
54 public class AppJmol extends StructureViewerBase
55 {
56   // ms to wait for Jmol to load files
57   private static final int JMOL_LOAD_TIMEOUT = 20000;
58
59   private static final String SPACE = " ";
60
61   private static final String BACKSLASH = "\"";
62
63   AppJmolBinding jmb;
64
65   JPanel scriptWindow;
66
67   JSplitPane splitPane;
68
69   RenderPanel renderPanel;
70
71   /**
72    * 
73    * @param files
74    * @param ids
75    * @param seqs
76    * @param ap
77    * @param usetoColour
78    *          - add the alignment panel to the list used for colouring these
79    *          structures
80    * @param useToAlign
81    *          - add the alignment panel to the list used for aligning these
82    *          structures
83    * @param leaveColouringToJmol
84    *          - do not update the colours from any other source. Jmol is
85    *          handling them
86    * @param loadStatus
87    * @param bounds
88    * @param viewid
89    */
90   public AppJmol(String[] files, String[] ids, SequenceI[][] seqs,
91           AlignmentPanel ap, boolean usetoColour, boolean useToAlign,
92           boolean leaveColouringToJmol, String loadStatus, Rectangle bounds,
93           String viewid)
94   {
95     PDBEntry[] pdbentrys = new PDBEntry[files.length];
96     for (int i = 0; i < pdbentrys.length; i++)
97     {
98       // PDBEntry pdbentry = new PDBEntry(files[i], ids[i]);
99       PDBEntry pdbentry = new PDBEntry(ids[i], null, PDBEntry.Type.PDB,
100               files[i]);
101       pdbentrys[i] = pdbentry;
102     }
103     // / TODO: check if protocol is needed to be set, and if chains are
104     // autodiscovered.
105     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
106             pdbentrys, seqs, null);
107
108     jmb.setLoadingFromArchive(true);
109     addAlignmentPanel(ap);
110     if (useToAlign)
111     {
112       useAlignmentPanelForSuperposition(ap);
113     }
114     initMenus();
115     if (leaveColouringToJmol || !usetoColour)
116     {
117       jmb.setColourBySequence(false);
118       seqColour.setSelected(false);
119       viewerColour.setSelected(true);
120     }
121     else if (usetoColour)
122     {
123       useAlignmentPanelForColourbyseq(ap);
124       jmb.setColourBySequence(true);
125       seqColour.setSelected(true);
126       viewerColour.setSelected(false);
127     }
128     this.setBounds(bounds);
129     setViewId(viewid);
130     // jalview.gui.Desktop.addInternalFrame(this, "Loading File",
131     // bounds.width,bounds.height);
132
133     this.addInternalFrameListener(new InternalFrameAdapter()
134     {
135       @Override
136       public void internalFrameClosing(
137               InternalFrameEvent internalFrameEvent)
138       {
139         closeViewer(false);
140       }
141     });
142     initJmol(loadStatus); // pdbentry, seq, JBPCHECK!
143   }
144
145   @Override
146   protected void initMenus()
147   {
148     super.initMenus();
149
150     viewerActionMenu.setText(MessageManager.getString("label.jmol"));
151
152     viewerColour
153             .setText(MessageManager.getString("label.colour_with_jmol"));
154     viewerColour.setToolTipText(MessageManager
155             .getString("label.let_jmol_manage_structure_colours"));
156   }
157
158   IProgressIndicator progressBar = null;
159
160   /**
161    * add a single PDB structure to a new or existing Jmol view
162    * 
163    * @param pdbentry
164    * @param seq
165    * @param chains
166    * @param ap
167    */
168   public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
169           final AlignmentPanel ap)
170   {
171     progressBar = ap.alignFrame;
172     String pdbId = pdbentry.getId();
173
174     /*
175      * If the PDB file is already loaded, the user may just choose to add to an
176      * existing viewer (or cancel)
177      */
178     if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
179     {
180       return;
181     }
182
183     /*
184      * Check if there are other Jmol views involving this alignment and prompt
185      * user about adding this molecule to one of them
186      */
187     if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
188     {
189       return;
190     }
191
192     /*
193      * If the options above are declined or do not apply, open a new viewer
194      */
195     openNewJmol(ap, new PDBEntry[] { pdbentry }, new SequenceI[][] { seq });
196   }
197
198   private void openNewJmol(AlignmentPanel ap, PDBEntry[] pdbentrys,
199           SequenceI[][] seqs)
200   {
201     progressBar = ap.alignFrame;
202     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
203             pdbentrys, seqs, null);
204     addAlignmentPanel(ap);
205     useAlignmentPanelForColourbyseq(ap);
206
207     if (pdbentrys.length > 1)
208     {
209       alignAddedStructures = true;
210       useAlignmentPanelForSuperposition(ap);
211     }
212     jmb.setColourBySequence(true);
213     setSize(400, 400); // probably should be a configurable/dynamic default here
214     initMenus();
215     addingStructures = false;
216     worker = new Thread(this);
217     worker.start();
218
219     this.addInternalFrameListener(new InternalFrameAdapter()
220     {
221       @Override
222       public void internalFrameClosing(
223               InternalFrameEvent internalFrameEvent)
224       {
225         closeViewer(false);
226       }
227     });
228
229   }
230
231   /**
232    * create a new Jmol containing several structures superimposed using the
233    * given alignPanel.
234    * 
235    * @param ap
236    * @param pe
237    * @param seqs
238    */
239   public AppJmol(AlignmentPanel ap, PDBEntry[] pe, SequenceI[][] seqs)
240   {
241     openNewJmol(ap, pe, seqs);
242   }
243
244   /**
245    * Returns a list of any Jmol viewers. The list is restricted to those linked
246    * to the given alignment panel if it is not null.
247    */
248   @Override
249   protected List<StructureViewerBase> getViewersFor(AlignmentPanel apanel)
250   {
251     List<StructureViewerBase> result = new ArrayList<StructureViewerBase>();
252     JInternalFrame[] frames = Desktop.instance.getAllFrames();
253
254     for (JInternalFrame frame : frames)
255     {
256       if (frame instanceof AppJmol)
257       {
258         if (apanel == null
259                 || ((StructureViewerBase) frame).isLinkedWith(apanel))
260         {
261           result.add((StructureViewerBase) frame);
262         }
263       }
264     }
265     return result;
266   }
267
268   void initJmol(String command)
269   {
270     jmb.setFinishedInit(false);
271     renderPanel = new RenderPanel();
272     // TODO: consider waiting until the structure/view is fully loaded before
273     // displaying
274     this.getContentPane().add(renderPanel, java.awt.BorderLayout.CENTER);
275     jalview.gui.Desktop.addInternalFrame(this, jmb.getViewerTitle(),
276             getBounds().width, getBounds().height);
277     if (scriptWindow == null)
278     {
279       BorderLayout bl = new BorderLayout();
280       bl.setHgap(0);
281       bl.setVgap(0);
282       scriptWindow = new JPanel(bl);
283       scriptWindow.setVisible(false);
284     }
285
286     jmb.allocateViewer(renderPanel, true, "", null, null, "", scriptWindow,
287             null);
288     // jmb.newJmolPopup("Jmol");
289     if (command == null)
290     {
291       command = "";
292     }
293     jmb.evalStateCommand(command);
294     jmb.evalStateCommand("set hoverDelay=0.1");
295     jmb.setFinishedInit(true);
296   }
297
298   boolean allChainsSelected = false;
299
300   @Override
301   void showSelectedChains()
302   {
303     Vector<String> toshow = new Vector<String>();
304     for (int i = 0; i < chainMenu.getItemCount(); i++)
305     {
306       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
307       {
308         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
309         if (item.isSelected())
310         {
311           toshow.addElement(item.getText());
312         }
313       }
314     }
315     jmb.centerViewer(toshow);
316   }
317
318   @Override
319   public void closeViewer(boolean closeExternalViewer)
320   {
321     // Jmol does not use an external viewer
322     if (jmb != null)
323     {
324       jmb.closeViewer();
325     }
326     setAlignmentPanel(null);
327     _aps.clear();
328     _alignwith.clear();
329     _colourwith.clear();
330     // TODO: check for memory leaks where instance isn't finalised because jmb
331     // holds a reference to the window
332     jmb = null;
333   }
334
335   @Override
336   public void run()
337   {
338     _started = true;
339     try
340     {
341       List<String> files = fetchPdbFiles();
342       if (files.size() > 0)
343       {
344         showFilesInViewer(files);
345       }
346     } finally
347     {
348       _started = false;
349       worker = null;
350     }
351   }
352
353   /**
354    * Either adds the given files to a structure viewer or opens a new viewer to
355    * show them
356    * 
357    * @param files
358    *          list of absolute paths to structure files
359    */
360   void showFilesInViewer(List<String> files)
361   {
362     long lastnotify = jmb.getLoadNotifiesHandled();
363     StringBuilder fileList = new StringBuilder();
364     for (String s : files)
365     {
366       fileList.append(SPACE).append(BACKSLASH)
367               .append(Platform.escapeString(s)).append(BACKSLASH);
368     }
369     String filesString = fileList.toString();
370
371     if (!addingStructures)
372     {
373       try
374       {
375         initJmol("load FILES " + filesString);
376       } catch (OutOfMemoryError oomerror)
377       {
378         new OOMWarning("When trying to open the Jmol viewer!", oomerror);
379         Cache.log.debug("File locations are " + filesString);
380       } catch (Exception ex)
381       {
382         Cache.log.error("Couldn't open Jmol viewer!", ex);
383       }
384     }
385     else
386     {
387       StringBuilder cmd = new StringBuilder();
388       cmd.append("loadingJalviewdata=true\nload APPEND ");
389       cmd.append(filesString);
390       cmd.append("\nloadingJalviewdata=null");
391       final String command = cmd.toString();
392       lastnotify = jmb.getLoadNotifiesHandled();
393
394       try
395       {
396         jmb.evalStateCommand(command);
397       } catch (OutOfMemoryError oomerror)
398       {
399         new OOMWarning("When trying to add structures to the Jmol viewer!",
400                 oomerror);
401         Cache.log.debug("File locations are " + filesString);
402       } catch (Exception ex)
403       {
404         Cache.log.error("Couldn't add files to Jmol viewer!", ex);
405       }
406     }
407
408     // need to wait around until script has finished
409     int waitMax = JMOL_LOAD_TIMEOUT;
410     int waitFor = 35;
411     int waitTotal = 0;
412     while (addingStructures ? lastnotify >= jmb.getLoadNotifiesHandled()
413             : !(jmb.isFinishedInit() && jmb.getStructureFiles() != null
414                     && jmb.getStructureFiles().length == files.size()))
415     {
416       try
417       {
418         Cache.log.debug("Waiting around for jmb notify.");
419         Thread.sleep(waitFor);
420         waitTotal += waitFor;
421       } catch (Exception e)
422       {
423       }
424       if (waitTotal > waitMax)
425       {
426         System.err.println("Timed out waiting for Jmol to load files after "
427                 + waitTotal + "ms");
428         // System.err.println("finished: " + jmb.isFinishedInit()
429         // + "; loaded: " + Arrays.toString(jmb.getPdbFile())
430         // + "; files: " + files.toString());
431         jmb.getStructureFiles();
432         break;
433       }
434     }
435
436     // refresh the sequence colours for the new structure(s)
437     for (AlignmentPanel ap : _colourwith)
438     {
439       jmb.updateColours(ap);
440     }
441     // do superposition if asked to
442     if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
443     {
444       alignAddedStructures();
445     }
446     addingStructures = false;
447   }
448
449   /**
450    * Queues a thread to align structures with Jalview alignments
451    */
452   void alignAddedStructures()
453   {
454     javax.swing.SwingUtilities.invokeLater(new Runnable()
455     {
456       @Override
457       public void run()
458       {
459         if (jmb.viewer.isScriptExecuting())
460         {
461           SwingUtilities.invokeLater(this);
462           try
463           {
464             Thread.sleep(5);
465           } catch (InterruptedException q)
466           {
467           }
468           return;
469         }
470         else
471         {
472           alignStructs_withAllAlignPanels();
473         }
474       }
475     });
476     alignAddedStructures = false;
477   }
478
479   /**
480    * Retrieves and saves as file any modelled PDB entries for which we do not
481    * already have a file saved. Returns a list of absolute paths to structure
482    * files which were either retrieved, or already stored but not modelled in
483    * the structure viewer (i.e. files to add to the viewer display).
484    * 
485    * @return
486    */
487   List<String> fetchPdbFiles()
488   {
489     // todo - record which pdbids were successfully imported.
490     StringBuilder errormsgs = new StringBuilder();
491
492     List<String> files = new ArrayList<String>();
493     String pdbid = "";
494     try
495     {
496       String[] filesInViewer = jmb.getStructureFiles();
497       // TODO: replace with reference fetching/transfer code (validate PDBentry
498       // as a DBRef?)
499       Pdb pdbclient = new Pdb();
500       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
501       {
502         String file = jmb.getPdbEntry(pi).getFile();
503         if (file == null)
504         {
505           // retrieve the pdb and store it locally
506           AlignmentI pdbseq = null;
507           pdbid = jmb.getPdbEntry(pi).getId();
508           long hdl = pdbid.hashCode() - System.currentTimeMillis();
509           if (progressBar != null)
510           {
511             progressBar.setProgressBar(MessageManager
512                     .formatMessage("status.fetching_pdb", new String[]
513                     { pdbid }), hdl);
514           }
515           try
516           {
517             pdbseq = pdbclient.getSequenceRecords(pdbid);
518           } catch (OutOfMemoryError oomerror)
519           {
520             new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
521           } catch (Exception ex)
522           {
523             ex.printStackTrace();
524             errormsgs.append("'").append(pdbid).append("'");
525           } finally
526           {
527             if (progressBar != null)
528             {
529               progressBar.setProgressBar(
530                       MessageManager.getString("label.state_completed"),
531                       hdl);
532             }
533           }
534           if (pdbseq != null)
535           {
536             // just transfer the file name from the first sequence's first
537             // PDBEntry
538             file = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
539                     .elementAt(0).getFile()).getAbsolutePath();
540             jmb.getPdbEntry(pi).setFile(file);
541             files.add(file);
542           }
543           else
544           {
545             errormsgs.append("'").append(pdbid).append("' ");
546           }
547         }
548         else
549         {
550           if (filesInViewer != null && filesInViewer.length > 0)
551           {
552             addingStructures = true; // already files loaded.
553             for (int c = 0; c < filesInViewer.length; c++)
554             {
555               if (filesInViewer[c].equals(file))
556               {
557                 file = null;
558                 break;
559               }
560             }
561           }
562           if (file != null)
563           {
564             files.add(file);
565           }
566         }
567       }
568     } catch (OutOfMemoryError oomerror)
569     {
570       new OOMWarning("Retrieving PDB files: " + pdbid, oomerror);
571     } catch (Exception ex)
572     {
573       ex.printStackTrace();
574       errormsgs.append("When retrieving pdbfiles : current was: '")
575               .append(pdbid).append("'");
576     }
577     if (errormsgs.length() > 0)
578     {
579       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
580               MessageManager.formatMessage(
581                       "label.pdb_entries_couldnt_be_retrieved", new String[]
582                       { errormsgs.toString() }),
583               MessageManager.getString("label.couldnt_load_file"),
584               JvOptionPane.ERROR_MESSAGE);
585     }
586     return files;
587   }
588
589   @Override
590   public void eps_actionPerformed(ActionEvent e)
591   {
592     makePDBImage(jalview.util.ImageMaker.TYPE.EPS);
593   }
594
595   @Override
596   public void png_actionPerformed(ActionEvent e)
597   {
598     makePDBImage(jalview.util.ImageMaker.TYPE.PNG);
599   }
600
601   void makePDBImage(jalview.util.ImageMaker.TYPE type)
602   {
603     int width = getWidth();
604     int height = getHeight();
605
606     jalview.util.ImageMaker im;
607
608     if (type == jalview.util.ImageMaker.TYPE.PNG)
609     {
610       im = new jalview.util.ImageMaker(this,
611               jalview.util.ImageMaker.TYPE.PNG, "Make PNG image from view",
612               width, height, null, null, null, 0, false);
613     }
614     else if (type == jalview.util.ImageMaker.TYPE.EPS)
615     {
616       im = new jalview.util.ImageMaker(this,
617               jalview.util.ImageMaker.TYPE.EPS, "Make EPS file from view",
618               width, height, null, this.getTitle(), null, 0, false);
619     }
620     else
621     {
622
623       im = new jalview.util.ImageMaker(this,
624               jalview.util.ImageMaker.TYPE.SVG, "Make SVG file from PCA",
625               width, height, null, this.getTitle(), null, 0, false);
626     }
627
628     if (im.getGraphics() != null)
629     {
630       jmb.viewer.renderScreenImage(im.getGraphics(), width, height);
631       im.writeImage();
632     }
633   }
634
635   @Override
636   public void showHelp_actionPerformed(ActionEvent actionEvent)
637   {
638     try
639     {
640       BrowserLauncher
641               .openURL("http://jmol.sourceforge.net/docs/JmolUserGuide/");
642     } catch (Exception ex)
643     {
644     }
645   }
646
647   public void showConsole(boolean showConsole)
648   {
649
650     if (showConsole)
651     {
652       if (splitPane == null)
653       {
654         splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
655         splitPane.setTopComponent(renderPanel);
656         splitPane.setBottomComponent(scriptWindow);
657         this.getContentPane().add(splitPane, BorderLayout.CENTER);
658         splitPane.setDividerLocation(getHeight() - 200);
659         scriptWindow.setVisible(true);
660         scriptWindow.validate();
661         splitPane.validate();
662       }
663
664     }
665     else
666     {
667       if (splitPane != null)
668       {
669         splitPane.setVisible(false);
670       }
671
672       splitPane = null;
673
674       this.getContentPane().add(renderPanel, BorderLayout.CENTER);
675     }
676
677     validate();
678   }
679
680   class RenderPanel extends JPanel
681   {
682     final Dimension currentSize = new Dimension();
683
684     @Override
685     public void paintComponent(Graphics g)
686     {
687       getSize(currentSize);
688
689       if (jmb != null && jmb.hasFileLoadingError())
690       {
691         g.setColor(Color.black);
692         g.fillRect(0, 0, currentSize.width, currentSize.height);
693         g.setColor(Color.white);
694         g.setFont(new Font("Verdana", Font.BOLD, 14));
695         g.drawString(MessageManager.getString("label.error_loading_file")
696                 + "...", 20, currentSize.height / 2);
697         StringBuffer sb = new StringBuffer();
698         int lines = 0;
699         for (int e = 0; e < jmb.getPdbCount(); e++)
700         {
701           sb.append(jmb.getPdbEntry(e).getId());
702           if (e < jmb.getPdbCount() - 1)
703           {
704             sb.append(",");
705           }
706
707           if (e == jmb.getPdbCount() - 1 || sb.length() > 20)
708           {
709             lines++;
710             g.drawString(sb.toString(), 20, currentSize.height / 2
711                     - lines * g.getFontMetrics().getHeight());
712           }
713         }
714       }
715       else if (jmb == null || jmb.viewer == null || !jmb.isFinishedInit())
716       {
717         g.setColor(Color.black);
718         g.fillRect(0, 0, currentSize.width, currentSize.height);
719         g.setColor(Color.white);
720         g.setFont(new Font("Verdana", Font.BOLD, 14));
721         g.drawString(MessageManager.getString("label.retrieving_pdb_data"),
722                 20, currentSize.height / 2);
723       }
724       else
725       {
726         jmb.viewer.renderScreenImage(g, currentSize.width,
727                 currentSize.height);
728       }
729     }
730   }
731
732   @Override
733   public AAStructureBindingModel getBinding()
734   {
735     return this.jmb;
736   }
737
738   @Override
739   public String getStateInfo()
740   {
741     return jmb == null ? null : jmb.viewer.getStateInfo();
742   }
743
744   @Override
745   public ViewerType getViewerType()
746   {
747     return ViewerType.JMOL;
748   }
749
750   @Override
751   protected String getViewerName()
752   {
753     return "Jmol";
754   }
755 }