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