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