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