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