JAL-4344 Some fixes of waiting blocks, and headless mode for export. Control logging...
[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.Graphics2D;
29 import java.awt.RenderingHints;
30 import java.io.File;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.concurrent.Executors;
35
36 import javax.swing.JPanel;
37 import javax.swing.JSplitPane;
38 import javax.swing.SwingUtilities;
39 import javax.swing.event.InternalFrameAdapter;
40 import javax.swing.event.InternalFrameEvent;
41
42 import jalview.api.AlignmentViewPanel;
43 import jalview.bin.Console;
44 import jalview.bin.Jalview;
45 import jalview.datamodel.PDBEntry;
46 import jalview.datamodel.SequenceI;
47 import jalview.datamodel.StructureViewerModel;
48 import jalview.datamodel.StructureViewerModel.StructureData;
49 import jalview.gui.ImageExporter.ImageWriterI;
50 import jalview.gui.StructureViewer.ViewerType;
51 import jalview.io.exceptions.ImageOutputException;
52 import jalview.structure.StructureCommand;
53 import jalview.structures.models.AAStructureBindingModel;
54 import jalview.util.BrowserLauncher;
55 import jalview.util.ImageMaker;
56 import jalview.util.ImageMaker.TYPE;
57 import jalview.util.MessageManager;
58 import jalview.util.Platform;
59 import jalview.util.imagemaker.BitmapImageSizing;
60
61 public class AppJmol extends StructureViewerBase
62 {
63   // ms to wait for Jmol to load files
64   private static final int JMOL_LOAD_TIMEOUT = 20000;
65
66   private static final String SPACE = " ";
67
68   private static final String QUOTE = "\"";
69
70   AppJmolBinding jmb;
71
72   JPanel scriptWindow;
73
74   JSplitPane splitPane;
75
76   RenderPanel renderPanel;
77
78   /**
79    * 
80    * @param files
81    * @param ids
82    * @param seqs
83    * @param ap
84    * @param usetoColour
85    *          - add the alignment panel to the list used for colouring these
86    *          structures
87    * @param useToAlign
88    *          - add the alignment panel to the list used for aligning these
89    *          structures
90    * @param leaveColouringToJmol
91    *          - do not update the colours from any other source. Jmol is
92    *          handling them
93    * @param loadStatus
94    * @param bounds
95    * @param viewid
96    */
97   public AppJmol(StructureViewerModel viewerModel, AlignmentPanel ap,
98           String sessionFile, String viewid)
99   {
100     Map<File, StructureData> pdbData = viewerModel.getFileData();
101     PDBEntry[] pdbentrys = new PDBEntry[pdbData.size()];
102     SequenceI[][] seqs = new SequenceI[pdbData.size()][];
103     int i = 0;
104     for (StructureData data : pdbData.values())
105     {
106       PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
107               PDBEntry.Type.PDB, data.getFilePath());
108       pdbentrys[i] = pdbentry;
109       List<SequenceI> sequencesForPdb = data.getSeqList();
110       seqs[i] = sequencesForPdb
111               .toArray(new SequenceI[sequencesForPdb.size()]);
112       i++;
113     }
114
115     // TODO: check if protocol is needed to be set, and if chains are
116     // autodiscovered.
117     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
118             pdbentrys, seqs, null);
119
120     jmb.setLoadingFromArchive(true);
121     addAlignmentPanel(ap);
122     if (viewerModel.isAlignWithPanel())
123     {
124       useAlignmentPanelForSuperposition(ap);
125     }
126     initMenus();
127     boolean useToColour = viewerModel.isColourWithAlignPanel();
128     boolean leaveColouringToJmol = viewerModel.isColourByViewer();
129     if (leaveColouringToJmol || !useToColour)
130     {
131       jmb.setColourBySequence(false);
132       seqColour.setSelected(false);
133       viewerColour.setSelected(true);
134     }
135     else if (useToColour)
136     {
137       useAlignmentPanelForColourbyseq(ap);
138       jmb.setColourBySequence(true);
139       seqColour.setSelected(true);
140       viewerColour.setSelected(false);
141     }
142
143     this.setBounds(viewerModel.getX(), viewerModel.getY(),
144             viewerModel.getWidth(), viewerModel.getHeight());
145     setViewId(viewid);
146
147     this.addInternalFrameListener(new InternalFrameAdapter()
148     {
149       @Override
150       public void internalFrameClosing(
151               InternalFrameEvent internalFrameEvent)
152       {
153         closeViewer(false);
154       }
155     });
156     StringBuilder cmd = new StringBuilder();
157     cmd.append("load FILES ").append(QUOTE)
158             .append(Platform.escapeBackslashes(sessionFile)).append(QUOTE);
159     initJmol(cmd.toString());
160   }
161
162   @Override
163   protected void initMenus()
164   {
165     super.initMenus();
166
167     viewerColour
168             .setText(MessageManager.getString("label.colour_with_jmol"));
169     viewerColour.setToolTipText(MessageManager
170             .getString("label.let_jmol_manage_structure_colours"));
171   }
172
173   /**
174    * display a single PDB structure in a new Jmol view
175    * 
176    * @param pdbentry
177    * @param seq
178    * @param chains
179    * @param ap
180    */
181   public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
182           final AlignmentPanel ap)
183   {
184     setProgressIndicator(ap.alignFrame);
185
186     openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry },
187             new SequenceI[][]
188             { seq });
189   }
190
191   private void openNewJmol(AlignmentPanel ap, boolean alignAdded,
192           PDBEntry[] pdbentrys, SequenceI[][] seqs)
193   {
194     setProgressIndicator(ap.alignFrame);
195     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
196             pdbentrys, seqs, null);
197     addAlignmentPanel(ap);
198     useAlignmentPanelForColourbyseq(ap);
199
200     alignAddedStructures = alignAdded;
201     if (pdbentrys.length > 1)
202     {
203       useAlignmentPanelForSuperposition(ap);
204     }
205
206     jmb.setColourBySequence(true);
207     setSize(400, 400); // probably should be a configurable/dynamic default here
208     initMenus();
209     addingStructures = false;
210     worker = new Thread(this);
211     worker.start();
212
213     this.addInternalFrameListener(new InternalFrameAdapter()
214     {
215       @Override
216       public void internalFrameClosing(
217               InternalFrameEvent internalFrameEvent)
218       {
219         closeViewer(false);
220       }
221     });
222
223   }
224
225   /**
226    * create a new Jmol containing several structures optionally superimposed
227    * using the given alignPanel.
228    * 
229    * @param ap
230    * @param alignAdded
231    *          - true to superimpose
232    * @param pe
233    * @param seqs
234    */
235   public AppJmol(AlignmentPanel ap, boolean alignAdded, PDBEntry[] pe,
236           SequenceI[][] seqs)
237   {
238     openNewJmol(ap, alignAdded, pe, seqs);
239   }
240
241   void initJmol(String command)
242   {
243     jmb.setFinishedInit(false);
244     renderPanel = new RenderPanel();
245     // TODO: consider waiting until the structure/view is fully loaded before
246     // displaying
247     this.getContentPane().add(renderPanel, java.awt.BorderLayout.CENTER);
248     jalview.gui.Desktop.addInternalFrame(this, jmb.getViewerTitle(),
249             getBounds().width, getBounds().height);
250     if (scriptWindow == null)
251     {
252       BorderLayout bl = new BorderLayout();
253       bl.setHgap(0);
254       bl.setVgap(0);
255       scriptWindow = new JPanel(bl);
256       scriptWindow.setVisible(false);
257     }
258
259     jmb.allocateViewer(renderPanel, true, "", null, null, "", scriptWindow,
260             null);
261     // jmb.newJmolPopup("Jmol");
262     if (command == null)
263     {
264       command = "";
265     }
266     jmb.executeCommand(new StructureCommand(command), false);
267     jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false);
268     jmb.executeCommand(new StructureCommand("set antialiasdisplay on"),
269             false);
270     jmb.setFinishedInit(true);
271   }
272
273   @Override
274   public void run()
275   {
276     _started = true;
277     try
278     {
279       List<String> files = jmb.fetchPdbFiles(this);
280       if (files.size() > 0)
281       {
282         showFilesInViewer(files);
283       }
284     } finally
285     {
286       _started = false;
287       worker = null;
288     }
289   }
290
291   /**
292    * Either adds the given files to a structure viewer or opens a new viewer to
293    * show them
294    * 
295    * @param files
296    *          list of absolute paths to structure files
297    */
298   void showFilesInViewer(List<String> files)
299   {
300     long lastnotify = jmb.getLoadNotifiesHandled();
301     StringBuilder fileList = new StringBuilder();
302     for (String s : files)
303     {
304       fileList.append(SPACE).append(QUOTE)
305               .append(Platform.escapeBackslashes(s)).append(QUOTE);
306     }
307     String filesString = fileList.toString();
308
309     if (!addingStructures)
310     {
311       try
312       {
313         initJmol("load FILES " + filesString);
314       } catch (OutOfMemoryError oomerror)
315       {
316         new OOMWarning("When trying to open the Jmol viewer!", oomerror);
317         Console.debug("File locations are " + filesString);
318       } catch (Exception ex)
319       {
320         Console.error("Couldn't open Jmol viewer!", ex);
321         ex.printStackTrace();
322         return;
323       }
324     }
325     else
326     {
327       StringBuilder cmd = new StringBuilder();
328       cmd.append("loadingJalviewdata=true\nload APPEND ");
329       cmd.append(filesString);
330       cmd.append("\nloadingJalviewdata=null");
331       final StructureCommand command = new StructureCommand(cmd.toString());
332       lastnotify = jmb.getLoadNotifiesHandled();
333
334       try
335       {
336         jmb.executeCommand(command, false);
337       } catch (OutOfMemoryError oomerror)
338       {
339         new OOMWarning("When trying to add structures to the Jmol viewer!",
340                 oomerror);
341         Console.debug("File locations are " + filesString);
342         return;
343       } catch (Exception ex)
344       {
345         Console.error("Couldn't add files to Jmol viewer!", ex);
346         ex.printStackTrace();
347         return;
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         Console.debug("Waiting around for jmb notify.");
362         waitTotal += waitFor;
363
364         // Thread.sleep() throws an exception in JS
365         Thread.sleep(waitFor);
366       } catch (Exception e)
367       {
368       }
369       if (waitTotal > waitMax)
370       {
371         jalview.bin.Console.errPrintln(
372                 "Timed out waiting for Jmol to load files after "
373                         + waitTotal + "ms");
374         // jalview.bin.Console.errPrintln("finished: " + jmb.isFinishedInit()
375         // + "; loaded: " + Arrays.toString(jmb.getPdbFile())
376         // + "; files: " + files.toString());
377         jmb.getStructureFiles();
378         break;
379       }
380     }
381
382     // refresh the sequence colours for the new structure(s)
383     for (AlignmentViewPanel ap : _colourwith)
384     {
385       jmb.updateColours(ap);
386     }
387     // do superposition if asked to
388     if (alignAddedStructures)
389     {
390       alignAddedStructures();
391     }
392     addingStructures = false;
393   }
394
395   /**
396    * Queues a thread to align structures with Jalview alignments
397    */
398   void alignAddedStructures()
399   {
400     javax.swing.SwingUtilities.invokeLater(new Runnable()
401     {
402       @Override
403       public void run()
404       {
405         if (jmb.jmolViewer.isScriptExecuting())
406         {
407           SwingUtilities.invokeLater(this);
408           try
409           {
410             Thread.sleep(5);
411           } catch (InterruptedException q)
412           {
413           }
414           return;
415         }
416         else
417         {
418           alignStructsWithAllAlignPanels();
419         }
420       }
421     });
422
423   }
424
425   /**
426    * Outputs the Jmol viewer image as an image file, after prompting the user to
427    * choose a file and (for EPS) choice of Text or Lineart character rendering
428    * (unless a preference for this is set)
429    * 
430    * @param type
431    */
432   @Override
433   public void makePDBImage(ImageMaker.TYPE type)
434   {
435     try
436     {
437       makePDBImage(null, type, null,
438               BitmapImageSizing.defaultBitmapImageSizing());
439     } catch (ImageOutputException ioex)
440     {
441       Console.error("Unexpected error whilst writing " + type.toString(),
442               ioex);
443     }
444   }
445
446   public void makePDBImage(File file, ImageMaker.TYPE type, String renderer,
447           BitmapImageSizing userBis) throws ImageOutputException
448   {
449     int width = getWidth();
450     int height = getHeight();
451
452     BitmapImageSizing bis = ImageMaker.getScaleWidthHeight(width, height,
453             userBis);
454     float usescale = bis.scale();
455     int usewidth = bis.width();
456     int useheight = bis.height();
457     ImageWriterI writer = new ImageWriterI()
458     {
459       @Override
460       public void exportImage(Graphics g) throws Exception
461       {
462         Graphics2D ig2 = (Graphics2D) g;
463         ig2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
464                 RenderingHints.VALUE_ANTIALIAS_ON);
465         if (type == TYPE.PNG && usescale > 0.0f)
466         {
467           // for a scaled image, this scales down a bigger image to give the
468           // right resolution
469           if (usescale > 0.0f)
470           {
471             ig2.scale(1 / usescale, 1 / usescale);
472           }
473         }
474
475         jmb.jmolViewer.requestRepaintAndWait("image export");
476         jmb.jmolViewer.renderScreenImage(ig2, usewidth, useheight);
477       }
478     };
479     String view = MessageManager.getString("action.view")
480             .toLowerCase(Locale.ROOT);
481     final ImageExporter exporter = new ImageExporter(writer,
482             getProgressIndicator(), type, getTitle());
483
484     final Throwable[] exceptions = new Throwable[1];
485     exceptions[0] = null;
486     final AppJmol us = this;
487     Runnable exportRunnable = () -> {
488       try
489       {
490         exporter.doExport(file, us, width, height, view, renderer, userBis);
491       } catch (Throwable t)
492       {
493         Console.debug("Problem when exporting structure image", t);
494         exceptions[0] = t;
495       }
496     };
497     try
498     {
499       if (Jalview.isHeadlessMode())
500       {
501         exportRunnable.run();
502       }
503       else
504       {
505         Thread runner = Executors.defaultThreadFactory()
506                 .newThread(exportRunnable);
507         runner.start();
508         long time = 0;
509         do
510         {
511           Thread.sleep(25);
512         } while (runner.isAlive() && time++ < 4000);
513         if (time >= 4000)
514         {
515           runner.interrupt();
516           throw new ImageOutputException(
517                   "Jmol took too long to export. Waited for 100 seconds.");
518         }
519       }
520     } catch (Throwable e)
521     {
522       throw new ImageOutputException(
523               "Unexpected error when generating image", e);
524     }
525     if (exceptions[0] != null)
526     {
527       if (exceptions[0] instanceof ImageOutputException)
528       {
529         throw ((ImageOutputException) exceptions[0]);
530       }
531       else
532       {
533         throw new ImageOutputException(
534                 "Unexpected error when generating image", exceptions[0]);
535       }
536     }
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       jalview.bin.Console
549               .errPrintln("Show Jmol help failed with: " + ex.getMessage());
550     }
551   }
552
553   @Override
554   public void showConsole(boolean showConsole)
555   {
556     if (showConsole)
557     {
558       if (splitPane == null)
559       {
560         splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
561         splitPane.setTopComponent(renderPanel);
562         splitPane.setBottomComponent(scriptWindow);
563         this.getContentPane().add(splitPane, BorderLayout.CENTER);
564         splitPane.setDividerLocation(getHeight() - 200);
565         scriptWindow.setVisible(true);
566         scriptWindow.validate();
567         splitPane.validate();
568       }
569
570     }
571     else
572     {
573       if (splitPane != null)
574       {
575         splitPane.setVisible(false);
576       }
577
578       splitPane = null;
579
580       this.getContentPane().add(renderPanel, BorderLayout.CENTER);
581     }
582
583     validate();
584   }
585
586   class RenderPanel extends JPanel
587   {
588     final Dimension currentSize = new Dimension();
589
590     @Override
591     public void paintComponent(Graphics g)
592     {
593       getSize(currentSize);
594
595       if (jmb != null && jmb.hasFileLoadingError())
596       {
597         g.setColor(Color.black);
598         g.fillRect(0, 0, currentSize.width, currentSize.height);
599         g.setColor(Color.white);
600         g.setFont(new Font("Verdana", Font.BOLD, 14));
601         g.drawString(MessageManager.getString("label.error_loading_file")
602                 + "...", 20, currentSize.height / 2);
603         StringBuffer sb = new StringBuffer();
604         int lines = 0;
605         for (int e = 0; e < jmb.getPdbCount(); e++)
606         {
607           sb.append(jmb.getPdbEntry(e).getId());
608           if (e < jmb.getPdbCount() - 1)
609           {
610             sb.append(",");
611           }
612
613           if (e == jmb.getPdbCount() - 1 || sb.length() > 20)
614           {
615             lines++;
616             g.drawString(sb.toString(), 20, currentSize.height / 2
617                     - lines * g.getFontMetrics().getHeight());
618           }
619         }
620       }
621       else if (jmb == null || jmb.jmolViewer == null
622               || !jmb.isFinishedInit())
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.retrieving_pdb_data"),
629                 20, currentSize.height / 2);
630       }
631       else
632       {
633         jmb.jmolViewer.renderScreenImage(g, currentSize.width,
634                 currentSize.height);
635       }
636     }
637   }
638
639   @Override
640   public AAStructureBindingModel getBinding()
641   {
642     return this.jmb;
643   }
644
645   @Override
646   public ViewerType getViewerType()
647   {
648     return ViewerType.JMOL;
649   }
650
651   @Override
652   protected String getViewerName()
653   {
654     return "Jmol";
655   }
656 }