JAL-3551 copy Jalview features to Pymol 'p' (with pull refactoring)
[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 java.awt.BorderLayout;
24 import java.awt.Color;
25 import java.awt.Dimension;
26 import java.awt.Font;
27 import java.awt.Graphics;
28 import java.awt.Rectangle;
29 import java.io.File;
30 import java.util.ArrayList;
31 import java.util.List;
32
33 import javax.swing.JPanel;
34 import javax.swing.JSplitPane;
35 import javax.swing.SwingUtilities;
36 import javax.swing.event.InternalFrameAdapter;
37 import javax.swing.event.InternalFrameEvent;
38
39 import jalview.api.AlignmentViewPanel;
40 import jalview.bin.Cache;
41 import jalview.datamodel.AlignmentI;
42 import jalview.datamodel.PDBEntry;
43 import jalview.datamodel.SequenceI;
44 import jalview.gui.ImageExporter.ImageWriterI;
45 import jalview.gui.StructureViewer.ViewerType;
46 import jalview.structure.StructureCommand;
47 import jalview.structures.models.AAStructureBindingModel;
48 import jalview.util.BrowserLauncher;
49 import jalview.util.ImageMaker;
50 import jalview.util.MessageManager;
51 import jalview.util.Platform;
52 import jalview.ws.dbsources.Pdb;
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     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     if (pdbentrys.length > 1)
186     {
187       useAlignmentPanelForSuperposition(ap);
188     }
189
190     jmb.setColourBySequence(true);
191     setSize(400, 400); // probably should be a configurable/dynamic default here
192     initMenus();
193     addingStructures = false;
194     worker = new Thread(this);
195     worker.start();
196
197     this.addInternalFrameListener(new InternalFrameAdapter()
198     {
199       @Override
200       public void internalFrameClosing(
201               InternalFrameEvent internalFrameEvent)
202       {
203         closeViewer(false);
204       }
205     });
206
207   }
208
209   /**
210    * create a new Jmol containing several structures optionally superimposed
211    * using the given alignPanel.
212    * 
213    * @param ap
214    * @param alignAdded
215    *          - true to superimpose
216    * @param pe
217    * @param seqs
218    */
219   public AppJmol(AlignmentPanel ap, boolean alignAdded, PDBEntry[] pe,
220           SequenceI[][] seqs)
221   {
222     openNewJmol(ap, alignAdded, 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.executeCommand(new StructureCommand(command), false);
252     jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false);
253     jmb.setFinishedInit(true);
254   }
255
256   @Override
257   public void run()
258   {
259     _started = true;
260     try
261     {
262       List<String> files = fetchPdbFiles();
263       if (files.size() > 0)
264       {
265         showFilesInViewer(files);
266       }
267     } finally
268     {
269       _started = false;
270       worker = null;
271     }
272   }
273
274   /**
275    * Either adds the given files to a structure viewer or opens a new viewer to
276    * show them
277    * 
278    * @param files
279    *          list of absolute paths to structure files
280    */
281   void showFilesInViewer(List<String> files)
282   {
283     long lastnotify = jmb.getLoadNotifiesHandled();
284     StringBuilder fileList = new StringBuilder();
285     for (String s : files)
286     {
287       fileList.append(SPACE).append(QUOTE)
288               .append(Platform.escapeBackslashes(s)).append(QUOTE);
289     }
290     String filesString = fileList.toString();
291
292     if (!addingStructures)
293     {
294       try
295       {
296         initJmol("load FILES " + filesString);
297       } catch (OutOfMemoryError oomerror)
298       {
299         new OOMWarning("When trying to open the Jmol viewer!", oomerror);
300         Cache.log.debug("File locations are " + filesString);
301       } catch (Exception ex)
302       {
303         Cache.log.error("Couldn't open Jmol viewer!", ex);
304         ex.printStackTrace();
305         return;
306       }
307     }
308     else
309     {
310       StringBuilder cmd = new StringBuilder();
311       cmd.append("loadingJalviewdata=true\nload APPEND ");
312       cmd.append(filesString);
313       cmd.append("\nloadingJalviewdata=null");
314       final StructureCommand command = new StructureCommand(cmd.toString());
315       lastnotify = jmb.getLoadNotifiesHandled();
316
317       try
318       {
319         jmb.executeCommand(command, false);
320       } catch (OutOfMemoryError oomerror)
321       {
322         new OOMWarning("When trying to add structures to the Jmol viewer!",
323                 oomerror);
324         Cache.log.debug("File locations are " + filesString);
325         return;
326       } catch (Exception ex)
327       {
328         Cache.log.error("Couldn't add files to Jmol viewer!", ex);
329         ex.printStackTrace();
330         return;
331       }
332     }
333
334     // need to wait around until script has finished
335     int waitMax = JMOL_LOAD_TIMEOUT;
336     int waitFor = 35;
337     int waitTotal = 0;
338     while (addingStructures ? lastnotify >= jmb.getLoadNotifiesHandled()
339             : !(jmb.isFinishedInit() && jmb.getStructureFiles() != null
340                     && jmb.getStructureFiles().length == files.size()))
341     {
342       try
343       {
344         Cache.log.debug("Waiting around for jmb notify.");
345         waitTotal += waitFor;
346
347         // Thread.sleep() throws an exception in JS
348         Thread.sleep(waitFor);
349       } catch (Exception e)
350       {
351       }
352       if (waitTotal > waitMax)
353       {
354         System.err.println("Timed out waiting for Jmol to load files after "
355                 + waitTotal + "ms");
356         // System.err.println("finished: " + jmb.isFinishedInit()
357         // + "; loaded: " + Arrays.toString(jmb.getPdbFile())
358         // + "; files: " + files.toString());
359         jmb.getStructureFiles();
360         break;
361       }
362     }
363
364     // refresh the sequence colours for the new structure(s)
365     for (AlignmentViewPanel ap : _colourwith)
366     {
367       jmb.updateColours(ap);
368     }
369     // do superposition if asked to
370     if (alignAddedStructures)
371     {
372       alignAddedStructures();
373     }
374     addingStructures = false;
375   }
376
377   /**
378    * Queues a thread to align structures with Jalview alignments
379    */
380   void alignAddedStructures()
381   {
382     javax.swing.SwingUtilities.invokeLater(new Runnable()
383     {
384       @Override
385       public void run()
386       {
387         if (jmb.jmolViewer.isScriptExecuting())
388         {
389           SwingUtilities.invokeLater(this);
390           try
391           {
392             Thread.sleep(5);
393           } catch (InterruptedException q)
394           {
395           }
396           return;
397         }
398         else
399         {
400           alignStructsWithAllAlignPanels();
401         }
402       }
403     });
404
405   }
406
407   /**
408    * Retrieves and saves as file any modelled PDB entries for which we do not
409    * already have a file saved. Returns a list of absolute paths to structure
410    * files which were either retrieved, or already stored but not modelled in
411    * the structure viewer (i.e. files to add to the viewer display).
412    * 
413    * @return
414    */
415   List<String> fetchPdbFiles()
416   {
417     // todo - record which pdbids were successfully imported.
418     StringBuilder errormsgs = new StringBuilder();
419
420     List<String> files = new ArrayList<>();
421     String pdbid = "";
422     try
423     {
424       String[] filesInViewer = jmb.getStructureFiles();
425       // TODO: replace with reference fetching/transfer code (validate PDBentry
426       // as a DBRef?)
427       Pdb pdbclient = new Pdb();
428       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
429       {
430         String file = jmb.getPdbEntry(pi).getFile();
431         if (file == null)
432         {
433           // todo: extract block as method and pull up (also ChimeraViewFrame)
434           // retrieve the pdb and store it locally
435           AlignmentI pdbseq = null;
436           pdbid = jmb.getPdbEntry(pi).getId();
437           long hdl = pdbid.hashCode() - System.currentTimeMillis();
438           setProgressMessage(MessageManager
439                   .formatMessage("status.fetching_pdb", new String[]
440                   { pdbid }), hdl);
441           try
442           {
443             pdbseq = pdbclient.getSequenceRecords(pdbid);
444           } catch (OutOfMemoryError oomerror)
445           {
446             new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
447           } catch (Exception ex)
448           {
449             ex.printStackTrace();
450             errormsgs.append("'").append(pdbid).append("'");
451           } finally
452           {
453             setProgressMessage(
454                     MessageManager.getString("label.state_completed"), hdl);
455           }
456           if (pdbseq != null)
457           {
458             // just transfer the file name from the first sequence's first
459             // PDBEntry
460             file = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
461                     .elementAt(0).getFile()).getAbsolutePath();
462             jmb.getPdbEntry(pi).setFile(file);
463             files.add(file);
464           }
465           else
466           {
467             errormsgs.append("'").append(pdbid).append("' ");
468           }
469         }
470         else
471         {
472           if (filesInViewer != null && filesInViewer.length > 0)
473           {
474             addingStructures = true; // already files loaded.
475             for (int c = 0; c < filesInViewer.length; c++)
476             {
477               if (Platform.pathEquals(filesInViewer[c], file))
478               {
479                 file = null;
480                 break;
481               }
482             }
483           }
484           if (file != null)
485           {
486             files.add(file);
487           }
488         }
489       }
490     } catch (OutOfMemoryError oomerror)
491     {
492       new OOMWarning("Retrieving PDB files: " + pdbid, oomerror);
493     } catch (Exception ex)
494     {
495       ex.printStackTrace();
496       errormsgs.append("When retrieving pdbfiles : current was: '")
497               .append(pdbid).append("'");
498     }
499     if (errormsgs.length() > 0)
500     {
501       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
502               MessageManager.formatMessage(
503                       "label.pdb_entries_couldnt_be_retrieved", new String[]
504                       { errormsgs.toString() }),
505               MessageManager.getString("label.couldnt_load_file"),
506               JvOptionPane.ERROR_MESSAGE);
507     }
508     return files;
509   }
510
511   /**
512    * Outputs the Jmol viewer image as an image file, after prompting the user to
513    * choose a file and (for EPS) choice of Text or Lineart character rendering
514    * (unless a preference for this is set)
515    * 
516    * @param type
517    */
518   @Override
519   public void makePDBImage(ImageMaker.TYPE type)
520   {
521     int width = getWidth();
522     int height = getHeight();
523     ImageWriterI writer = new ImageWriterI()
524     {
525       @Override
526       public void exportImage(Graphics g) throws Exception
527       {
528         jmb.jmolViewer.renderScreenImage(g, width, height);
529       }
530     };
531     String view = MessageManager.getString("action.view").toLowerCase();
532     ImageExporter exporter = new ImageExporter(writer,
533             getProgressIndicator(), type, getTitle());
534     exporter.doExport(null, this, width, height, view);
535   }
536
537   @Override
538   public void showHelp_actionPerformed()
539   {
540     try
541     {
542       BrowserLauncher // BH 2018
543               .openURL("http://wiki.jmol.org");//http://jmol.sourceforge.net/docs/JmolUserGuide/");
544     } catch (Exception ex)
545     {
546       System.err.println("Show Jmol help failed with: " + ex.getMessage());
547     }
548   }
549
550   @Override
551   public void showConsole(boolean showConsole)
552   {
553     if (showConsole)
554     {
555       if (splitPane == null)
556       {
557         splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
558         splitPane.setTopComponent(renderPanel);
559         splitPane.setBottomComponent(scriptWindow);
560         this.getContentPane().add(splitPane, BorderLayout.CENTER);
561         splitPane.setDividerLocation(getHeight() - 200);
562         scriptWindow.setVisible(true);
563         scriptWindow.validate();
564         splitPane.validate();
565       }
566
567     }
568     else
569     {
570       if (splitPane != null)
571       {
572         splitPane.setVisible(false);
573       }
574
575       splitPane = null;
576
577       this.getContentPane().add(renderPanel, BorderLayout.CENTER);
578     }
579
580     validate();
581   }
582
583   class RenderPanel extends JPanel
584   {
585     final Dimension currentSize = new Dimension();
586
587     @Override
588     public void paintComponent(Graphics g)
589     {
590       getSize(currentSize);
591
592       if (jmb != null && jmb.hasFileLoadingError())
593       {
594         g.setColor(Color.black);
595         g.fillRect(0, 0, currentSize.width, currentSize.height);
596         g.setColor(Color.white);
597         g.setFont(new Font("Verdana", Font.BOLD, 14));
598         g.drawString(MessageManager.getString("label.error_loading_file")
599                 + "...", 20, currentSize.height / 2);
600         StringBuffer sb = new StringBuffer();
601         int lines = 0;
602         for (int e = 0; e < jmb.getPdbCount(); e++)
603         {
604           sb.append(jmb.getPdbEntry(e).getId());
605           if (e < jmb.getPdbCount() - 1)
606           {
607             sb.append(",");
608           }
609
610           if (e == jmb.getPdbCount() - 1 || sb.length() > 20)
611           {
612             lines++;
613             g.drawString(sb.toString(), 20, currentSize.height / 2
614                     - lines * g.getFontMetrics().getHeight());
615           }
616         }
617       }
618       else if (jmb == null || jmb.jmolViewer == null || !jmb.isFinishedInit())
619       {
620         g.setColor(Color.black);
621         g.fillRect(0, 0, currentSize.width, currentSize.height);
622         g.setColor(Color.white);
623         g.setFont(new Font("Verdana", Font.BOLD, 14));
624         g.drawString(MessageManager.getString("label.retrieving_pdb_data"),
625                 20, currentSize.height / 2);
626       }
627       else
628       {
629         jmb.jmolViewer.renderScreenImage(g, currentSize.width,
630                 currentSize.height);
631       }
632     }
633   }
634
635   @Override
636   public AAStructureBindingModel getBinding()
637   {
638     return this.jmb;
639   }
640
641   @Override
642   public ViewerType getViewerType()
643   {
644     return ViewerType.JMOL;
645   }
646
647   @Override
648   protected String getViewerName()
649   {
650     return "Jmol";
651   }
652 }