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