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