JAL-2446 merged to spike branch
[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,
93           Rectangle bounds, 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(InternalFrameEvent internalFrameEvent)
137       {
138         closeViewer(false);
139       }
140     });
141     initJmol(loadStatus); // pdbentry, seq, JBPCHECK!
142   }
143
144   @Override
145   protected void initMenus()
146   {
147     super.initMenus();
148
149     viewerActionMenu.setText(MessageManager.getString("label.jmol"));
150
151     viewerColour
152             .setText(MessageManager.getString("label.colour_with_jmol"));
153     viewerColour.setToolTipText(MessageManager
154             .getString("label.let_jmol_manage_structure_colours"));
155   }
156
157   IProgressIndicator progressBar = null;
158
159   /**
160    * add a single PDB structure to a new or existing Jmol view
161    * 
162    * @param pdbentry
163    * @param seq
164    * @param chains
165    * @param ap
166    */
167   public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
168           final AlignmentPanel ap)
169   {
170     progressBar = ap.alignFrame;
171     String pdbId = pdbentry.getId();
172
173     /*
174      * If the PDB file is already loaded, the user may just choose to add to an
175      * existing viewer (or cancel)
176      */
177     if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
178     {
179       return;
180     }
181
182     /*
183      * Check if there are other Jmol views involving this alignment and prompt
184      * user about adding this molecule to one of them
185      */
186     if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
187     {
188       return;
189     }
190
191     /*
192      * If the options above are declined or do not apply, open a new viewer
193      */
194     openNewJmol(ap, new PDBEntry[] { pdbentry }, new SequenceI[][] { seq });
195   }
196
197   private void openNewJmol(AlignmentPanel ap, PDBEntry[] pdbentrys,
198           SequenceI[][] seqs)
199   {
200     progressBar = ap.alignFrame;
201     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
202             pdbentrys, seqs, null);
203     addAlignmentPanel(ap);
204     useAlignmentPanelForColourbyseq(ap);
205
206     if (pdbentrys.length > 1)
207     {
208       alignAddedStructures = true;
209       useAlignmentPanelForSuperposition(ap);
210     }
211     jmb.setColourBySequence(true);
212     setSize(400, 400); // probably should be a configurable/dynamic default here
213     initMenus();
214     addingStructures = false;
215     worker = new Thread(this);
216     worker.start();
217
218     this.addInternalFrameListener(new InternalFrameAdapter()
219     {
220       @Override
221       public void internalFrameClosing(InternalFrameEvent internalFrameEvent)
222       {
223         closeViewer(false);
224       }
225     });
226
227   }
228
229   /**
230    * create a new Jmol containing several structures superimposed using the
231    * given alignPanel.
232    * 
233    * @param ap
234    * @param pe
235    * @param seqs
236    */
237   public AppJmol(AlignmentPanel ap, PDBEntry[] pe, SequenceI[][] seqs)
238   {
239     openNewJmol(ap, pe, seqs);
240   }
241
242   /**
243    * Returns a list of any Jmol viewers. The list is restricted to those linked
244    * to the given alignment panel if it is not null.
245    */
246   @Override
247   protected List<StructureViewerBase> getViewersFor(AlignmentPanel apanel)
248   {
249     List<StructureViewerBase> result = new ArrayList<StructureViewerBase>();
250     JInternalFrame[] frames = Desktop.instance.getAllFrames();
251
252     for (JInternalFrame frame : frames)
253     {
254       if (frame instanceof AppJmol)
255       {
256         if (apanel == null
257                 || ((StructureViewerBase) frame).isLinkedWith(apanel))
258         {
259           result.add((StructureViewerBase) frame);
260         }
261       }
262     }
263     return result;
264   }
265
266   void initJmol(String command)
267   {
268     jmb.setFinishedInit(false);
269     renderPanel = new RenderPanel();
270     // TODO: consider waiting until the structure/view is fully loaded before
271     // displaying
272     this.getContentPane().add(renderPanel, java.awt.BorderLayout.CENTER);
273     jalview.gui.Desktop.addInternalFrame(this, jmb.getViewerTitle(),
274             getBounds().width, getBounds().height);
275     if (scriptWindow == null)
276     {
277       BorderLayout bl = new BorderLayout();
278       bl.setHgap(0);
279       bl.setVgap(0);
280       scriptWindow = new JPanel(bl);
281       scriptWindow.setVisible(false);
282     }
283
284     jmb.allocateViewer(renderPanel, true, "", null, null, "",
285             scriptWindow, null);
286     // jmb.newJmolPopup("Jmol");
287     if (command == null)
288     {
289       command = "";
290     }
291     jmb.evalStateCommand(command);
292     jmb.evalStateCommand("set hoverDelay=0.1");
293     jmb.setFinishedInit(true);
294   }
295
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 && jmb
414                     .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
427                 .println("Timed out waiting for Jmol to load files after "
428                         + waitTotal + "ms");
429 //        System.err.println("finished: " + jmb.isFinishedInit()
430 //                + "; loaded: " + Arrays.toString(jmb.getPdbFile())
431 //                + "; files: " + files.toString());
432         jmb.getStructureFiles();
433         break;
434       }
435     }
436
437     // refresh the sequence colours for the new structure(s)
438     for (AlignmentPanel ap : _colourwith)
439     {
440       jmb.updateColours(ap);
441     }
442     // do superposition if asked to
443     if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
444     {
445       alignAddedStructures();
446     }
447     addingStructures = false;
448   }
449
450   /**
451    * Queues a thread to align structures with Jalview alignments
452    */
453   void alignAddedStructures()
454   {
455     javax.swing.SwingUtilities.invokeLater(new Runnable()
456     {
457       @Override
458       public void run()
459       {
460         if (jmb.viewer.isScriptExecuting())
461         {
462           SwingUtilities.invokeLater(this);
463           try
464           {
465             Thread.sleep(5);
466           } catch (InterruptedException q)
467           {
468           }
469           return;
470         }
471         else
472         {
473           alignStructs_withAllAlignPanels();
474         }
475       }
476     });
477     alignAddedStructures = false;
478   }
479
480   /**
481    * Retrieves and saves as file any modelled PDB entries for which we do not
482    * already have a file saved. Returns a list of absolute paths to structure
483    * files which were either retrieved, or already stored but not modelled in
484    * the structure viewer (i.e. files to add to the viewer display).
485    * 
486    * @return
487    */
488   List<String> fetchPdbFiles()
489   {
490     // todo - record which pdbids were successfully imported.
491     StringBuilder errormsgs = new StringBuilder();
492
493     List<String> files = new ArrayList<String>();
494     String pdbid = "";
495     try
496     {
497       String[] filesInViewer = jmb.getStructureFiles();
498       // TODO: replace with reference fetching/transfer code (validate PDBentry
499       // as a DBRef?)
500       Pdb pdbclient = new Pdb();
501       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
502       {
503         String file = jmb.getPdbEntry(pi).getFile();
504         if (file == null)
505         {
506           // retrieve the pdb and store it locally
507           AlignmentI pdbseq = null;
508           pdbid = jmb.getPdbEntry(pi).getId();
509           long hdl = pdbid.hashCode() - System.currentTimeMillis();
510           if (progressBar != null)
511           {
512             progressBar.setProgressBar(MessageManager.formatMessage(
513                     "status.fetching_pdb", new String[] { 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, MessageManager
580               .formatMessage("label.pdb_entries_couldnt_be_retrieved",
581                       new String[] { errormsgs.toString() }),
582               MessageManager.getString("label.couldnt_load_file"),
583               JvOptionPane.ERROR_MESSAGE);
584     }
585     return files;
586   }
587
588   @Override
589   public void eps_actionPerformed(ActionEvent e)
590   {
591     makePDBImage(jalview.util.ImageMaker.TYPE.EPS);
592   }
593
594   @Override
595   public void png_actionPerformed(ActionEvent e)
596   {
597     makePDBImage(jalview.util.ImageMaker.TYPE.PNG);
598   }
599
600   void makePDBImage(jalview.util.ImageMaker.TYPE type)
601   {
602     int width = getWidth();
603     int height = getHeight();
604
605     jalview.util.ImageMaker im;
606
607     if (type == jalview.util.ImageMaker.TYPE.PNG)
608     {
609       im = new jalview.util.ImageMaker(this,
610               jalview.util.ImageMaker.TYPE.PNG, "Make PNG image from view",
611               width, height, null, null, null, 0, false);
612     }
613     else if (type == jalview.util.ImageMaker.TYPE.EPS)
614     {
615       im = new jalview.util.ImageMaker(this,
616               jalview.util.ImageMaker.TYPE.EPS, "Make EPS file from view",
617               width, height, null, this.getTitle(), null, 0, false);
618     }
619     else
620     {
621
622       im = new jalview.util.ImageMaker(this,
623               jalview.util.ImageMaker.TYPE.SVG, "Make SVG file from PCA",
624               width, height, null, this.getTitle(), null, 0, false);
625     }
626
627     if (im.getGraphics() != null)
628     {
629       jmb.viewer.renderScreenImage(im.getGraphics(), width, height);
630       im.writeImage();
631     }
632   }
633
634   @Override
635   public void showHelp_actionPerformed(ActionEvent actionEvent)
636   {
637     try
638     {
639       BrowserLauncher
640               .openURL("http://jmol.sourceforge.net/docs/JmolUserGuide/");
641     } catch (Exception ex)
642     {
643     }
644   }
645
646   public void showConsole(boolean showConsole)
647   {
648
649     if (showConsole)
650     {
651       if (splitPane == null)
652       {
653         splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
654         splitPane.setTopComponent(renderPanel);
655         splitPane.setBottomComponent(scriptWindow);
656         this.getContentPane().add(splitPane, BorderLayout.CENTER);
657         splitPane.setDividerLocation(getHeight() - 200);
658         scriptWindow.setVisible(true);
659         scriptWindow.validate();
660         splitPane.validate();
661       }
662
663     }
664     else
665     {
666       if (splitPane != null)
667       {
668         splitPane.setVisible(false);
669       }
670
671       splitPane = null;
672
673       this.getContentPane().add(renderPanel, BorderLayout.CENTER);
674     }
675
676     validate();
677   }
678
679   class RenderPanel extends JPanel
680   {
681     final Dimension currentSize = new Dimension();
682
683     @Override
684     public void paintComponent(Graphics g)
685     {
686       getSize(currentSize);
687
688       if (jmb != null && jmb.hasFileLoadingError())
689       {
690         g.setColor(Color.black);
691         g.fillRect(0, 0, currentSize.width, currentSize.height);
692         g.setColor(Color.white);
693         g.setFont(new Font("Verdana", Font.BOLD, 14));
694         g.drawString(MessageManager.getString("label.error_loading_file")
695                 + "...", 20, currentSize.height / 2);
696         StringBuffer sb = new StringBuffer();
697         int lines = 0;
698         for (int e = 0; e < jmb.getPdbCount(); e++)
699         {
700           sb.append(jmb.getPdbEntry(e).getId());
701           if (e < jmb.getPdbCount() - 1)
702           {
703             sb.append(",");
704           }
705
706           if (e == jmb.getPdbCount() - 1 || sb.length() > 20)
707           {
708             lines++;
709             g.drawString(sb.toString(), 20, currentSize.height / 2 - lines
710                     * g.getFontMetrics().getHeight());
711           }
712         }
713       }
714       else if (jmb == null || jmb.viewer == null || !jmb.isFinishedInit())
715       {
716         g.setColor(Color.black);
717         g.fillRect(0, 0, currentSize.width, currentSize.height);
718         g.setColor(Color.white);
719         g.setFont(new Font("Verdana", Font.BOLD, 14));
720         g.drawString(MessageManager.getString("label.retrieving_pdb_data"),
721                 20, currentSize.height / 2);
722       }
723       else
724       {
725         jmb.viewer.renderScreenImage(g, currentSize.width,
726                 currentSize.height);
727       }
728     }
729   }
730
731   @Override
732   public AAStructureBindingModel getBinding()
733   {
734     return this.jmb;
735   }
736
737   @Override
738   public String getStateInfo()
739   {
740     return jmb == null ? null : jmb.viewer.getStateInfo();
741   }
742
743   @Override
744   public ViewerType getViewerType()
745   {
746     return ViewerType.JMOL;
747   }
748
749   @Override
750   protected String getViewerName()
751   {
752     return "Jmol";
753   }
754 }