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