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