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