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