JAL-3390 pull-up refactoring towards functional changes...
[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 import java.util.Vector;
46
47 import javax.swing.JCheckBoxMenuItem;
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 QUOTE = "\"";
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   /**
167    * display a single PDB structure in a new Jmol view
168    * 
169    * @param pdbentry
170    * @param seq
171    * @param chains
172    * @param ap
173    */
174   public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
175           final AlignmentPanel ap)
176   {
177     progressBar = ap.alignFrame;
178
179     openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry },
180             new SequenceI[][]
181             { seq });
182   }
183
184   private void openNewJmol(AlignmentPanel ap, boolean alignAdded,
185           PDBEntry[] pdbentrys,
186           SequenceI[][] seqs)
187   {
188     progressBar = ap.alignFrame;
189     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
190             pdbentrys, seqs, null);
191     addAlignmentPanel(ap);
192     useAlignmentPanelForColourbyseq(ap);
193
194     alignAddedStructures = alignAdded;
195     useAlignmentPanelForSuperposition(ap);
196
197     jmb.setColourBySequence(true);
198     setSize(400, 400); // probably should be a configurable/dynamic default here
199     initMenus();
200     addingStructures = false;
201     worker = new Thread(this);
202     worker.start();
203
204     this.addInternalFrameListener(new InternalFrameAdapter()
205     {
206       @Override
207       public void internalFrameClosing(
208               InternalFrameEvent internalFrameEvent)
209       {
210         closeViewer(false);
211       }
212     });
213
214   }
215
216   /**
217    * create a new Jmol containing several structures optionally superimposed
218    * using the given alignPanel.
219    * 
220    * @param ap
221    * @param alignAdded
222    *          - true to superimpose
223    * @param pe
224    * @param seqs
225    */
226   public AppJmol(AlignmentPanel ap, boolean alignAdded, PDBEntry[] pe,
227           SequenceI[][] seqs)
228   {
229     openNewJmol(ap, alignAdded, pe, seqs);
230   }
231
232
233   void initJmol(String command)
234   {
235     jmb.setFinishedInit(false);
236     renderPanel = new RenderPanel();
237     // TODO: consider waiting until the structure/view is fully loaded before
238     // displaying
239     this.getContentPane().add(renderPanel, java.awt.BorderLayout.CENTER);
240     jalview.gui.Desktop.addInternalFrame(this, jmb.getViewerTitle(),
241             getBounds().width, getBounds().height);
242     if (scriptWindow == null)
243     {
244       BorderLayout bl = new BorderLayout();
245       bl.setHgap(0);
246       bl.setVgap(0);
247       scriptWindow = new JPanel(bl);
248       scriptWindow.setVisible(false);
249     }
250
251     jmb.allocateViewer(renderPanel, true, "", null, null, "", scriptWindow,
252             null);
253     // jmb.newJmolPopup("Jmol");
254     if (command == null)
255     {
256       command = "";
257     }
258     jmb.evalStateCommand(command);
259     jmb.evalStateCommand("set hoverDelay=0.1");
260     jmb.setFinishedInit(true);
261   }
262
263   @Override
264   void showSelectedChains()
265   {
266     Vector<String> toshow = new Vector<>();
267     for (int i = 0; i < chainMenu.getItemCount(); i++)
268     {
269       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
270       {
271         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
272         if (item.isSelected())
273         {
274           toshow.addElement(item.getText());
275         }
276       }
277     }
278     jmb.centerViewer(toshow);
279   }
280
281   @Override
282   public void closeViewer(boolean closeExternalViewer)
283   {
284     // Jmol does not use an external viewer
285     if (jmb != null)
286     {
287       jmb.closeViewer();
288     }
289     setAlignmentPanel(null);
290     _aps.clear();
291     _alignwith.clear();
292     _colourwith.clear();
293     // TODO: check for memory leaks where instance isn't finalised because jmb
294     // holds a reference to the window
295     jmb = null;
296   }
297
298   @Override
299   public void run()
300   {
301     _started = true;
302     try
303     {
304       List<String> files = fetchPdbFiles();
305       if (files.size() > 0)
306       {
307         showFilesInViewer(files);
308       }
309     } finally
310     {
311       _started = false;
312       worker = null;
313     }
314   }
315
316   /**
317    * Either adds the given files to a structure viewer or opens a new viewer to
318    * show them
319    * 
320    * @param files
321    *          list of absolute paths to structure files
322    */
323   void showFilesInViewer(List<String> files)
324   {
325     long lastnotify = jmb.getLoadNotifiesHandled();
326     StringBuilder fileList = new StringBuilder();
327     for (String s : files)
328     {
329       fileList.append(SPACE).append(QUOTE)
330               .append(Platform.escapeBackslashes(s)).append(QUOTE);
331     }
332     String filesString = fileList.toString();
333
334     if (!addingStructures)
335     {
336       try
337       {
338         initJmol("load FILES " + filesString);
339       } catch (OutOfMemoryError oomerror)
340       {
341         new OOMWarning("When trying to open the Jmol viewer!", oomerror);
342         Cache.log.debug("File locations are " + filesString);
343       } catch (Exception ex)
344       {
345         Cache.log.error("Couldn't open Jmol viewer!", ex);
346       }
347     }
348     else
349     {
350       StringBuilder cmd = new StringBuilder();
351       cmd.append("loadingJalviewdata=true\nload APPEND ");
352       cmd.append(filesString);
353       cmd.append("\nloadingJalviewdata=null");
354       final String command = cmd.toString();
355       lastnotify = jmb.getLoadNotifiesHandled();
356
357       try
358       {
359         jmb.evalStateCommand(command);
360       } catch (OutOfMemoryError oomerror)
361       {
362         new OOMWarning("When trying to add structures to the Jmol viewer!",
363                 oomerror);
364         Cache.log.debug("File locations are " + filesString);
365       } catch (Exception ex)
366       {
367         Cache.log.error("Couldn't add files to Jmol viewer!", ex);
368       }
369     }
370
371     // need to wait around until script has finished
372     int waitMax = JMOL_LOAD_TIMEOUT;
373     int waitFor = 35;
374     int waitTotal = 0;
375     while (addingStructures ? lastnotify >= jmb.getLoadNotifiesHandled()
376             : !(jmb.isFinishedInit() && jmb.getStructureFiles() != null
377                     && jmb.getStructureFiles().length == files.size()))
378     {
379       try
380       {
381         Cache.log.debug("Waiting around for jmb notify.");
382         Thread.sleep(waitFor);
383         waitTotal += waitFor;
384       } catch (Exception e)
385       {
386       }
387       if (waitTotal > waitMax)
388       {
389         System.err.println("Timed out waiting for Jmol to load files after "
390                 + waitTotal + "ms");
391         // System.err.println("finished: " + jmb.isFinishedInit()
392         // + "; loaded: " + Arrays.toString(jmb.getPdbFile())
393         // + "; files: " + files.toString());
394         jmb.getStructureFiles();
395         break;
396       }
397     }
398
399     // refresh the sequence colours for the new structure(s)
400     for (AlignmentViewPanel avp : _colourwith)
401     {
402       jmb.updateColours(avp);
403     }
404     // do superposition if asked to
405     if (alignAddedStructures)
406     {
407       alignAddedStructures();
408     }
409     addingStructures = false;
410   }
411
412   /**
413    * Queues a thread to align structures with Jalview alignments
414    */
415   void alignAddedStructures()
416   {
417     javax.swing.SwingUtilities.invokeLater(new Runnable()
418     {
419       @Override
420       public void run()
421       {
422         if (jmb.viewer.isScriptExecuting())
423         {
424           SwingUtilities.invokeLater(this);
425           try
426           {
427             Thread.sleep(5);
428           } catch (InterruptedException q)
429           {
430           }
431           return;
432         }
433         else
434         {
435           alignStructs_withAllAlignPanels();
436         }
437       }
438     });
439
440   }
441
442   /**
443    * Retrieves and saves as file any modelled PDB entries for which we do not
444    * already have a file saved. Returns a list of absolute paths to structure
445    * files which were either retrieved, or already stored but not modelled in
446    * the structure viewer (i.e. files to add to the viewer display).
447    * 
448    * @return
449    */
450   List<String> fetchPdbFiles()
451   {
452     // todo - record which pdbids were successfully imported.
453     StringBuilder errormsgs = new StringBuilder();
454
455     List<String> files = new ArrayList<>();
456     String pdbid = "";
457     try
458     {
459       String[] filesInViewer = jmb.getStructureFiles();
460       // TODO: replace with reference fetching/transfer code (validate PDBentry
461       // as a DBRef?)
462       Pdb pdbclient = new Pdb();
463       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
464       {
465         String file = jmb.getPdbEntry(pi).getFile();
466         if (file == null)
467         {
468           // todo: extract block as method and pull up (also ChimeraViewFrame)
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 (Platform.pathEquals(filesInViewer[c], 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 }