Merge branch 'JAL-3878_ws-overhaul-3' into with_ws_overhaul-3
[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.util.Locale;
24
25 import java.awt.BorderLayout;
26 import java.awt.Color;
27 import java.awt.Dimension;
28 import java.awt.Font;
29 import java.awt.Graphics;
30 import java.io.File;
31 import java.util.List;
32 import java.util.Map;
33
34 import javax.swing.JPanel;
35 import javax.swing.JSplitPane;
36 import javax.swing.SwingUtilities;
37 import javax.swing.event.InternalFrameAdapter;
38 import javax.swing.event.InternalFrameEvent;
39
40 import jalview.api.AlignmentViewPanel;
41 import jalview.bin.Console;
42 import jalview.datamodel.PDBEntry;
43 import jalview.datamodel.SequenceI;
44 import jalview.datamodel.StructureViewerModel;
45 import jalview.datamodel.StructureViewerModel.StructureData;
46 import jalview.fts.service.alphafold.AlphafoldRestClient;
47 import jalview.gui.ImageExporter.ImageWriterI;
48 import jalview.gui.StructureViewer.ViewerType;
49 import jalview.structure.StructureCommand;
50 import jalview.structures.models.AAStructureBindingModel;
51 import jalview.util.BrowserLauncher;
52 import jalview.util.ImageMaker;
53 import jalview.util.MessageManager;
54 import jalview.util.Platform;
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(StructureViewerModel viewerModel, AlignmentPanel ap,
92           String sessionFile, String viewid)
93   {
94     Map<File, StructureData> pdbData = viewerModel.getFileData();
95     PDBEntry[] pdbentrys = new PDBEntry[pdbData.size()];
96     SequenceI[][] seqs = new SequenceI[pdbData.size()][];
97     int i = 0;
98     for (StructureData data : pdbData.values())
99     {
100       PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
101               PDBEntry.Type.PDB, data.getFilePath());
102       pdbentrys[i] = pdbentry;
103       List<SequenceI> sequencesForPdb = data.getSeqList();
104       seqs[i] = sequencesForPdb
105               .toArray(new SequenceI[sequencesForPdb.size()]);
106       i++;
107     }
108     // TODO: check if protocol is needed to be set, and if chains are
109     // autodiscovered.
110     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
111             pdbentrys, seqs, null);
112
113     jmb.setLoadingFromArchive(true);
114     addAlignmentPanel(ap);
115     if (viewerModel.isAlignWithPanel())
116     {
117       useAlignmentPanelForSuperposition(ap);
118     }
119     initMenus();
120     boolean useToColour = viewerModel.isColourWithAlignPanel();
121     boolean leaveColouringToJmol = viewerModel.isColourByViewer();
122     if (leaveColouringToJmol || !useToColour)
123     {
124       jmb.setColourBySequence(false);
125       seqColour.setSelected(false);
126       viewerColour.setSelected(true);
127     }
128     else if (useToColour)
129     {
130       useAlignmentPanelForColourbyseq(ap);
131       jmb.setColourBySequence(true);
132       seqColour.setSelected(true);
133       viewerColour.setSelected(false);
134     }
135     this.setBounds(viewerModel.getX(), viewerModel.getY(),
136             viewerModel.getWidth(), viewerModel.getHeight());
137     setViewId(viewid);
138
139     this.addInternalFrameListener(new InternalFrameAdapter()
140     {
141       @Override
142       public void internalFrameClosing(
143               InternalFrameEvent internalFrameEvent)
144       {
145         closeViewer(false);
146       }
147     });
148     StringBuilder cmd = new StringBuilder();
149     cmd.append("load FILES ").append(QUOTE)
150             .append(Platform.escapeBackslashes(sessionFile)).append(QUOTE);
151     initJmol(cmd.toString());
152   }
153
154   @Override
155   protected void initMenus()
156   {
157     super.initMenus();
158
159
160     viewerColour
161             .setText(MessageManager.getString("label.colour_with_jmol"));
162     viewerColour.setToolTipText(MessageManager
163             .getString("label.let_jmol_manage_structure_colours"));
164   }
165
166   /**
167    * display a single PDB structure in a new Jmol view
168    * 
169    * @param pdbentry
170    * @param seq
171    * @param chains
172    * @param ap
173    */
174   public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
175           final AlignmentPanel ap)
176   {
177     setProgressIndicator(ap.alignFrame);
178
179     openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry },
180             new SequenceI[][]
181             { seq });
182   }
183
184   private void openNewJmol(AlignmentPanel ap, boolean alignAdded,
185           PDBEntry[] pdbentrys, SequenceI[][] seqs)
186   {
187     setProgressIndicator(ap.alignFrame);
188     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
189             pdbentrys, seqs, null);
190     addAlignmentPanel(ap);
191     useAlignmentPanelForColourbyseq(ap);
192
193     alignAddedStructures = alignAdded;
194     if (pdbentrys.length > 1)
195     {
196       useAlignmentPanelForSuperposition(ap);
197     }
198
199     jmb.setColourBySequence(true);
200     setSize(400, 400); // probably should be a configurable/dynamic default here
201     initMenus();
202     addingStructures = false;
203     worker = new Thread(this);
204     worker.start();
205
206     this.addInternalFrameListener(new InternalFrameAdapter()
207     {
208       @Override
209       public void internalFrameClosing(
210               InternalFrameEvent internalFrameEvent)
211       {
212         closeViewer(false);
213       }
214     });
215
216   }
217
218   /**
219    * create a new Jmol containing several structures optionally superimposed
220    * using the given alignPanel.
221    * 
222    * @param ap
223    * @param alignAdded
224    *          - true to superimpose
225    * @param pe
226    * @param seqs
227    */
228   public AppJmol(AlignmentPanel ap, boolean alignAdded, PDBEntry[] pe,
229           SequenceI[][] seqs)
230   {
231     openNewJmol(ap, alignAdded, pe, seqs);
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.executeCommand(new StructureCommand(command), false);
260     jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false);
261     jmb.setFinishedInit(true);
262   }
263
264   @Override
265   public void run()
266   {
267     _started = true;
268     try
269     {
270       List<String> files = jmb.fetchPdbFiles(this);
271       if (files.size() > 0)
272       {
273         showFilesInViewer(files);
274       }
275     } finally
276     {
277       _started = false;
278       worker = null;
279     }
280   }
281
282   /**
283    * Either adds the given files to a structure viewer or opens a new viewer to
284    * show them
285    * 
286    * @param files
287    *          list of absolute paths to structure files
288    */
289   void showFilesInViewer(List<String> files)
290   {
291     long lastnotify = jmb.getLoadNotifiesHandled();
292     StringBuilder fileList = new StringBuilder();
293     for (String s : files)
294     {
295       fileList.append(SPACE).append(QUOTE)
296               .append(Platform.escapeBackslashes(s)).append(QUOTE);
297     }
298     String filesString = fileList.toString();
299
300     if (!addingStructures)
301     {
302       try
303       {
304         initJmol("load FILES " + filesString);
305       } catch (OutOfMemoryError oomerror)
306       {
307         new OOMWarning("When trying to open the Jmol viewer!", oomerror);
308         Console.debug("File locations are " + filesString);
309       } catch (Exception ex)
310       {
311         Console.error("Couldn't open Jmol viewer!", ex);
312         ex.printStackTrace();
313         return;
314       }
315     }
316     else
317     {
318       StringBuilder cmd = new StringBuilder();
319       cmd.append("loadingJalviewdata=true\nload APPEND ");
320       cmd.append(filesString);
321       cmd.append("\nloadingJalviewdata=null");
322       final StructureCommand command = new StructureCommand(cmd.toString());
323       lastnotify = jmb.getLoadNotifiesHandled();
324
325       try
326       {
327         jmb.executeCommand(command, false);
328       } catch (OutOfMemoryError oomerror)
329       {
330         new OOMWarning("When trying to add structures to the Jmol viewer!",
331                 oomerror);
332         Console.debug("File locations are " + filesString);
333         return;
334       } catch (Exception ex)
335       {
336         Console.error("Couldn't add files to Jmol viewer!", ex);
337         ex.printStackTrace();
338         return;
339       }
340     }
341
342     // need to wait around until script has finished
343     int waitMax = JMOL_LOAD_TIMEOUT;
344     int waitFor = 35;
345     int waitTotal = 0;
346     while (addingStructures ? lastnotify >= jmb.getLoadNotifiesHandled()
347             : !(jmb.isFinishedInit() && jmb.getStructureFiles() != null
348                     && jmb.getStructureFiles().length == files.size()))
349     {
350       try
351       {
352         Console.debug("Waiting around for jmb notify.");
353         waitTotal += waitFor;
354
355         // Thread.sleep() throws an exception in JS
356         Thread.sleep(waitFor);
357       } catch (Exception e)
358       {
359       }
360       if (waitTotal > waitMax)
361       {
362         System.err.println("Timed out waiting for Jmol to load files after "
363                 + waitTotal + "ms");
364         // System.err.println("finished: " + jmb.isFinishedInit()
365         // + "; loaded: " + Arrays.toString(jmb.getPdbFile())
366         // + "; files: " + files.toString());
367         jmb.getStructureFiles();
368         break;
369       }
370     }
371
372     // refresh the sequence colours for the new structure(s)
373     for (AlignmentViewPanel ap : _colourwith)
374     {
375       jmb.updateColours(ap);
376     }
377     // do superposition if asked to
378     if (alignAddedStructures)
379     {
380       alignAddedStructures();
381     }
382     addingStructures = false;
383   }
384
385   /**
386    * Queues a thread to align structures with Jalview alignments
387    */
388   void alignAddedStructures()
389   {
390     javax.swing.SwingUtilities.invokeLater(new Runnable()
391     {
392       @Override
393       public void run()
394       {
395         if (jmb.jmolViewer.isScriptExecuting())
396         {
397           SwingUtilities.invokeLater(this);
398           try
399           {
400             Thread.sleep(5);
401           } catch (InterruptedException q)
402           {
403           }
404           return;
405         }
406         else
407         {
408           alignStructsWithAllAlignPanels();
409         }
410       }
411     });
412
413   }
414
415   /**
416    * Outputs the Jmol viewer image as an image file, after prompting the user to
417    * choose a file and (for EPS) choice of Text or Lineart character rendering
418    * (unless a preference for this is set)
419    * 
420    * @param type
421    */
422   @Override
423   public void makePDBImage(ImageMaker.TYPE type)
424   {
425     int width = getWidth();
426     int height = getHeight();
427     ImageWriterI writer = new ImageWriterI()
428     {
429       @Override
430       public void exportImage(Graphics g) throws Exception
431       {
432         jmb.jmolViewer.renderScreenImage(g, width, height);
433       }
434     };
435     String view = MessageManager.getString("action.view")
436             .toLowerCase(Locale.ROOT);
437     ImageExporter exporter = new ImageExporter(writer,
438             getProgressIndicator(), type, getTitle());
439     exporter.doExport(null, this, width, height, view);
440   }
441
442   @Override
443   public void showHelp_actionPerformed()
444   {
445     try
446     {
447       BrowserLauncher // BH 2018
448               .openURL("http://wiki.jmol.org");// http://jmol.sourceforge.net/docs/JmolUserGuide/");
449     } catch (Exception ex)
450     {
451       System.err.println("Show Jmol help failed with: " + ex.getMessage());
452     }
453   }
454
455   @Override
456   public void showConsole(boolean showConsole)
457   {
458
459     if (showConsole)
460     {
461       if (splitPane == null)
462       {
463         splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
464         splitPane.setTopComponent(renderPanel);
465         splitPane.setBottomComponent(scriptWindow);
466         this.getContentPane().add(splitPane, BorderLayout.CENTER);
467         splitPane.setDividerLocation(getHeight() - 200);
468         scriptWindow.setVisible(true);
469         scriptWindow.validate();
470         splitPane.validate();
471       }
472
473     }
474     else
475     {
476       if (splitPane != null)
477       {
478         splitPane.setVisible(false);
479       }
480
481       splitPane = null;
482
483       this.getContentPane().add(renderPanel, BorderLayout.CENTER);
484     }
485
486     validate();
487   }
488
489   class RenderPanel extends JPanel
490   {
491     final Dimension currentSize = new Dimension();
492
493     @Override
494     public void paintComponent(Graphics g)
495     {
496       getSize(currentSize);
497
498       if (jmb != null && jmb.hasFileLoadingError())
499       {
500         g.setColor(Color.black);
501         g.fillRect(0, 0, currentSize.width, currentSize.height);
502         g.setColor(Color.white);
503         g.setFont(new Font("Verdana", Font.BOLD, 14));
504         g.drawString(MessageManager.getString("label.error_loading_file")
505                 + "...", 20, currentSize.height / 2);
506         StringBuffer sb = new StringBuffer();
507         int lines = 0;
508         for (int e = 0; e < jmb.getPdbCount(); e++)
509         {
510           sb.append(jmb.getPdbEntry(e).getId());
511           if (e < jmb.getPdbCount() - 1)
512           {
513             sb.append(",");
514           }
515
516           if (e == jmb.getPdbCount() - 1 || sb.length() > 20)
517           {
518             lines++;
519             g.drawString(sb.toString(), 20, currentSize.height / 2
520                     - lines * g.getFontMetrics().getHeight());
521           }
522         }
523       }
524       else if (jmb == null || jmb.jmolViewer == null
525               || !jmb.isFinishedInit())
526       {
527         g.setColor(Color.black);
528         g.fillRect(0, 0, currentSize.width, currentSize.height);
529         g.setColor(Color.white);
530         g.setFont(new Font("Verdana", Font.BOLD, 14));
531         g.drawString(MessageManager.getString("label.retrieving_pdb_data"),
532                 20, currentSize.height / 2);
533       }
534       else
535       {
536         jmb.jmolViewer.renderScreenImage(g, currentSize.width,
537                 currentSize.height);
538       }
539     }
540   }
541
542   @Override
543   public AAStructureBindingModel getBinding()
544   {
545     return this.jmb;
546   }
547
548   @Override
549   public ViewerType getViewerType()
550   {
551     return ViewerType.JMOL;
552   }
553
554   @Override
555   protected String getViewerName()
556   {
557     return "Jmol";
558   }
559 }