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