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