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