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