Allows Jmol to share cached files
[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, null); // 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
203     this.addInternalFrameListener(new InternalFrameAdapter()
204     {
205       @Override
206       public void internalFrameClosing(
207               InternalFrameEvent internalFrameEvent)
208       {
209         closeViewer(false);
210       }
211     });
212
213     worker = new Thread(this);
214     worker.start();
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   private void initJmol(String command, List<File> files)
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     } else if (files != null) {
259                 jmb.cacheFiles(files);
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<File> 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<File> files)
327   {
328     long lastnotify = jmb.getLoadNotifiesHandled();
329     StringBuilder fileList = new StringBuilder();
330     for (File f : files)
331     {
332       String s = f.getAbsolutePath();
333       fileList.append(SPACE).append(QUOTE)
334               .append(Platform.escapeString(s)).append(QUOTE);
335     }
336     String filesString = fileList.toString();
337
338     if (!addingStructures)
339     {
340       try
341       {
342         initJmol("load FILES " + filesString, files);
343       } catch (OutOfMemoryError oomerror)
344       {
345         new OOMWarning("When trying to open the Jmol viewer!", oomerror);
346         Cache.log.debug("File locations are " + filesString);
347       } catch (Exception ex)
348       {
349         Cache.log.error("Couldn't open Jmol viewer!", ex);
350         ex.printStackTrace();
351         return;
352       }
353     }
354     else
355     {
356       StringBuilder cmd = new StringBuilder();
357       cmd.append("loadingJalviewdata=true\nload APPEND ");
358       cmd.append(filesString);
359       cmd.append("\nloadingJalviewdata=null");
360       final String command = cmd.toString();
361       lastnotify = jmb.getLoadNotifiesHandled();
362
363       try
364       {
365         jmb.cacheFiles(files);
366         jmb.evalStateCommand(command);
367       } catch (OutOfMemoryError oomerror)
368       {
369         new OOMWarning("When trying to add structures to the Jmol viewer!",
370                 oomerror);
371         Cache.log.debug("File locations are " + filesString);
372         return;
373       } catch (Exception ex)
374       {
375         Cache.log.error("Couldn't add files to Jmol viewer!", ex);
376         ex.printStackTrace();
377         return;
378       }
379     }
380
381     // need to wait around until script has finished
382     int waitMax = JMOL_LOAD_TIMEOUT;
383     int waitFor = 35;
384     int waitTotal = 0;
385     while (addingStructures ? lastnotify >= jmb.getLoadNotifiesHandled()
386             : !(jmb.isFinishedInit() && jmb.getStructureFiles() != null
387                     && jmb.getStructureFiles().length == files.size()))
388     {
389       try
390       {
391         Cache.log.debug("Waiting around for jmb notify.");
392         Thread.sleep(waitFor);
393         waitTotal += waitFor;
394       } catch (Exception e)
395       {
396       }
397       if (waitTotal > waitMax)
398       {
399         System.err.println("Timed out waiting for Jmol to load files after "
400                 + waitTotal + "ms");
401         // System.err.println("finished: " + jmb.isFinishedInit()
402         // + "; loaded: " + Arrays.toString(jmb.getPdbFile())
403         // + "; files: " + files.toString());
404         jmb.getStructureFiles();
405         break;
406       }
407     }
408
409     // refresh the sequence colours for the new structure(s)
410     for (AlignmentPanel ap : _colourwith)
411     {
412       jmb.updateColours(ap);
413     }
414     // do superposition if asked to
415     if (alignAddedStructures)
416     {
417       alignAddedStructures();
418     }
419     addingStructures = false;
420   }
421
422   /**
423    * Queues a thread to align structures with Jalview alignments
424    */
425   void alignAddedStructures()
426   {
427     javax.swing.SwingUtilities.invokeLater(new Runnable()
428     {
429       @Override
430       public void run()
431       {
432         if (jmb.viewer.isScriptExecuting())
433         {
434           SwingUtilities.invokeLater(this);
435           try
436           {
437             Thread.sleep(5);
438           } catch (InterruptedException q)
439           {
440           }
441           return;
442         }
443         else
444         {
445           alignStructs_withAllAlignPanels();
446         }
447       }
448     });
449
450   }
451
452   /**
453    * Retrieves and saves as file any modelled PDB entries for which we do not
454    * already have a file saved. Returns a list of absolute paths to structure
455    * files which were either retrieved, or already stored but not modelled in
456    * the structure viewer (i.e. files to add to the viewer display).
457    * 
458    * @return
459    */
460   List<File> fetchPdbFiles()
461   {
462     // todo - record which pdbids were successfully imported.
463     StringBuilder errormsgs = new StringBuilder();
464
465     List<File> files = new ArrayList<>();
466     String pdbid = "";
467     try
468     {
469       String[] filesInViewer = jmb.getStructureFiles();
470       // TODO: replace with reference fetching/transfer code (validate PDBentry
471       // as a DBRef?)
472       Pdb pdbclient = new Pdb();
473       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
474       {
475         File file = jmb.getPdbEntry(pi).getFileF();
476         if (file == null)
477         {
478           // todo: extract block as method and pull up (also ChimeraViewFrame)
479           // retrieve the pdb and store it locally
480           AlignmentI pdbseq = null;
481           pdbid = jmb.getPdbEntry(pi).getId();
482           long hdl = pdbid.hashCode() - System.currentTimeMillis();
483           if (progressBar != null)
484           {
485             progressBar.setProgressBar(MessageManager
486                     .formatMessage("status.fetching_pdb", new String[]
487                     { pdbid }), hdl);
488           }
489           try
490           {
491             pdbseq = pdbclient.getSequenceRecords(pdbid);
492           } catch (OutOfMemoryError oomerror)
493           {
494             new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
495           } catch (Exception ex)
496           {
497             ex.printStackTrace();
498             errormsgs.append("'").append(pdbid).append("'");
499           } finally
500           {
501             if (progressBar != null)
502             {
503               progressBar.setProgressBar(
504                       MessageManager.getString("label.state_completed"),
505                       hdl);
506             }
507           }
508           if (pdbseq != null)
509           {
510             // just transfer the file name from the first sequence's first
511             // PDBEntry
512             file = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
513                     .elementAt(0).getFile());
514             jmb.getPdbEntry(pi).setFile(file);
515             files.add(file);
516           }
517           else
518           {
519             errormsgs.append("'").append(pdbid).append("' ");
520           }
521         }
522         else
523         {
524           if (filesInViewer != null && filesInViewer.length > 0)
525           {
526             addingStructures = true; // already files loaded.
527             String filename = file.getAbsolutePath();
528             for (int c = 0; c < filesInViewer.length; c++)
529             {
530               if (filesInViewer[c].equals(filename))
531               {
532                 file = null;
533                 break;
534               }
535             }
536           }
537           if (file != null)
538           {
539             files.add(file);
540           }
541         }
542       }
543     } catch (OutOfMemoryError oomerror)
544     {
545       new OOMWarning("Retrieving PDB files: " + pdbid, oomerror);
546     } catch (Exception ex)
547     {
548       ex.printStackTrace();
549       errormsgs.append("When retrieving pdbfiles : current was: '")
550               .append(pdbid).append("'");
551     }
552     if (errormsgs.length() > 0)
553     {
554       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
555               MessageManager.formatMessage(
556                       "label.pdb_entries_couldnt_be_retrieved", new String[]
557                       { errormsgs.toString() }),
558               MessageManager.getString("label.couldnt_load_file"),
559               JvOptionPane.ERROR_MESSAGE);
560     }
561     return files;
562   }
563
564   /**
565    * Outputs the Jmol viewer image as an image file, after prompting the user to
566    * choose a file and (for EPS) choice of Text or Lineart character rendering
567    * (unless a preference for this is set)
568    * 
569    * @param type
570    */
571   public void makePDBImage(ImageMaker.TYPE type)
572   {
573     int width = getWidth();
574     int height = getHeight();
575     ImageWriterI writer = new ImageWriterI()
576     {
577       @Override
578       public void exportImage(Graphics g) throws Exception
579       {
580         jmb.viewer.renderScreenImage(g, width, height);
581       }
582     };
583     String view = MessageManager.getString("action.view").toLowerCase();
584     ImageExporter exporter = new ImageExporter(writer,
585             jmb.getIProgressIndicator(), type, getTitle());
586     exporter.doExport(null, this, width, height, view);
587   }
588
589   @Override
590   public void showHelp_actionPerformed(ActionEvent actionEvent)
591   {
592     try
593     {
594       BrowserLauncher // BH 2018
595               .openURL("http://wiki.jmol.org");//http://jmol.sourceforge.net/docs/JmolUserGuide/");
596     } catch (Exception ex)
597     {
598     }
599   }
600
601   public void showConsole(boolean showConsole)
602   {
603
604     if (showConsole)
605     {
606       if (splitPane == null)
607       {
608         splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
609         splitPane.setTopComponent(renderPanel);
610         splitPane.setBottomComponent(scriptWindow);
611         this.getContentPane().add(splitPane, BorderLayout.CENTER);
612         splitPane.setDividerLocation(getHeight() - 200);
613         scriptWindow.setVisible(true);
614         scriptWindow.validate();
615         splitPane.validate();
616       }
617
618     }
619     else
620     {
621       if (splitPane != null)
622       {
623         splitPane.setVisible(false);
624       }
625
626       splitPane = null;
627
628       this.getContentPane().add(renderPanel, BorderLayout.CENTER);
629     }
630
631     validate();
632   }
633
634   class RenderPanel extends JPanel
635   {
636     final Dimension currentSize = new Dimension();
637
638     @Override
639     public void paintComponent(Graphics g)
640     {
641       getSize(currentSize);
642
643       if (jmb != null && jmb.hasFileLoadingError())
644       {
645         g.setColor(Color.black);
646         g.fillRect(0, 0, currentSize.width, currentSize.height);
647         g.setColor(Color.white);
648         g.setFont(new Font("Verdana", Font.BOLD, 14));
649         g.drawString(MessageManager.getString("label.error_loading_file")
650                 + "...", 20, currentSize.height / 2);
651         StringBuffer sb = new StringBuffer();
652         int lines = 0;
653         for (int e = 0; e < jmb.getPdbCount(); e++)
654         {
655           sb.append(jmb.getPdbEntry(e).getId());
656           if (e < jmb.getPdbCount() - 1)
657           {
658             sb.append(",");
659           }
660
661           if (e == jmb.getPdbCount() - 1 || sb.length() > 20)
662           {
663             lines++;
664             g.drawString(sb.toString(), 20, currentSize.height / 2
665                     - lines * g.getFontMetrics().getHeight());
666           }
667         }
668       }
669       else if (jmb == null || jmb.viewer == null || !jmb.isFinishedInit())
670       {
671         g.setColor(Color.black);
672         g.fillRect(0, 0, currentSize.width, currentSize.height);
673         g.setColor(Color.white);
674         g.setFont(new Font("Verdana", Font.BOLD, 14));
675         g.drawString(MessageManager.getString("label.retrieving_pdb_data"),
676                 20, currentSize.height / 2);
677       }
678       else
679       {
680         jmb.viewer.renderScreenImage(g, currentSize.width,
681                 currentSize.height);
682       }
683     }
684   }
685
686   @Override
687   public AAStructureBindingModel getBinding()
688   {
689     return this.jmb;
690   }
691
692   @Override
693   public String getStateInfo()
694   {
695     return jmb == null ? null : jmb.viewer.getStateInfo();
696   }
697
698   @Override
699   public ViewerType getViewerType()
700   {
701     return ViewerType.JMOL;
702   }
703
704   @Override
705   protected String getViewerName()
706   {
707     return "Jmol";
708   }
709 }