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