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