JAL-3829 generalised Alphafold retrieval mechanism to be reused for PDBEntrys with...
[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.io.File;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Map;
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.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 import jalview.ws.dbsources.EBIAlfaFold;
56 import jalview.ws.dbsources.Pdb;
57 import jalview.ws.utils.UrlDownloadClient;
58
59 public class AppJmol extends StructureViewerBase
60 {
61   // ms to wait for Jmol to load files
62   private static final int JMOL_LOAD_TIMEOUT = 20000;
63
64   private static final String SPACE = " ";
65
66   private static final String QUOTE = "\"";
67
68   AppJmolBinding jmb;
69
70   JPanel scriptWindow;
71
72   JSplitPane splitPane;
73
74   RenderPanel renderPanel;
75
76   /**
77    * 
78    * @param files
79    * @param ids
80    * @param seqs
81    * @param ap
82    * @param usetoColour
83    *          - add the alignment panel to the list used for colouring these
84    *          structures
85    * @param useToAlign
86    *          - add the alignment panel to the list used for aligning these
87    *          structures
88    * @param leaveColouringToJmol
89    *          - do not update the colours from any other source. Jmol is
90    *          handling them
91    * @param loadStatus
92    * @param bounds
93    * @param viewid
94    */
95   public AppJmol(StructureViewerModel viewerModel, AlignmentPanel ap,
96           String sessionFile, String viewid)
97   {
98     Map<File, StructureData> pdbData = viewerModel.getFileData();
99     PDBEntry[] pdbentrys = new PDBEntry[pdbData.size()];
100     SequenceI[][] seqs = new SequenceI[pdbData.size()][];
101     int i = 0;
102     for (StructureData data : pdbData.values())
103     {
104       PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
105               PDBEntry.Type.PDB, data.getFilePath());
106       pdbentrys[i] = pdbentry;
107       List<SequenceI> sequencesForPdb = data.getSeqList();
108       seqs[i] = sequencesForPdb
109               .toArray(new SequenceI[sequencesForPdb.size()]);
110       i++;
111     }
112
113     // TODO: check if protocol is needed to be set, and if chains are
114     // autodiscovered.
115     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
116             pdbentrys, seqs, null);
117
118     jmb.setLoadingFromArchive(true);
119     addAlignmentPanel(ap);
120     if (viewerModel.isAlignWithPanel())
121     {
122       useAlignmentPanelForSuperposition(ap);
123     }
124     initMenus();
125     boolean useToColour = viewerModel.isColourWithAlignPanel();
126     boolean leaveColouringToJmol = viewerModel.isColourByViewer();
127     if (leaveColouringToJmol || !useToColour)
128     {
129       jmb.setColourBySequence(false);
130       seqColour.setSelected(false);
131       viewerColour.setSelected(true);
132     }
133     else if (useToColour)
134     {
135       useAlignmentPanelForColourbyseq(ap);
136       jmb.setColourBySequence(true);
137       seqColour.setSelected(true);
138       viewerColour.setSelected(false);
139     }
140
141     this.setBounds(viewerModel.getX(), viewerModel.getY(),
142             viewerModel.getWidth(), viewerModel.getHeight());
143     setViewId(viewid);
144
145     this.addInternalFrameListener(new InternalFrameAdapter()
146     {
147       @Override
148       public void internalFrameClosing(
149               InternalFrameEvent internalFrameEvent)
150       {
151         closeViewer(false);
152       }
153     });
154     StringBuilder cmd = new StringBuilder();
155     cmd.append("load FILES ").append(QUOTE)
156             .append(Platform.escapeBackslashes(sessionFile)).append(QUOTE);
157     initJmol(cmd.toString());
158   }
159
160   @Override
161   protected void initMenus()
162   {
163     super.initMenus();
164
165     viewerColour
166             .setText(MessageManager.getString("label.colour_with_jmol"));
167     viewerColour.setToolTipText(MessageManager
168             .getString("label.let_jmol_manage_structure_colours"));
169   }
170
171   /**
172    * display a single PDB structure in a new Jmol view
173    * 
174    * @param pdbentry
175    * @param seq
176    * @param chains
177    * @param ap
178    */
179   public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
180           final AlignmentPanel ap)
181   {
182     setProgressIndicator(ap.alignFrame);
183
184     openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry },
185             new SequenceI[][]
186             { seq });
187   }
188
189   private void openNewJmol(AlignmentPanel ap, boolean alignAdded,
190           PDBEntry[] pdbentrys,
191           SequenceI[][] seqs)
192   {
193     setProgressIndicator(ap.alignFrame);
194     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
195             pdbentrys, seqs, null);
196     addAlignmentPanel(ap);
197     useAlignmentPanelForColourbyseq(ap);
198
199     alignAddedStructures = alignAdded;
200     if (pdbentrys.length > 1)
201     {
202       useAlignmentPanelForSuperposition(ap);
203     }
204
205     jmb.setColourBySequence(true);
206     setSize(400, 400); // probably should be a configurable/dynamic default here
207     initMenus();
208     addingStructures = false;
209     worker = new Thread(this);
210     worker.start();
211
212     this.addInternalFrameListener(new InternalFrameAdapter()
213     {
214       @Override
215       public void internalFrameClosing(
216               InternalFrameEvent internalFrameEvent)
217       {
218         closeViewer(false);
219       }
220     });
221
222   }
223
224   /**
225    * create a new Jmol containing several structures optionally superimposed
226    * using the given alignPanel.
227    * 
228    * @param ap
229    * @param alignAdded
230    *          - true to superimpose
231    * @param pe
232    * @param seqs
233    */
234   public AppJmol(AlignmentPanel ap, boolean alignAdded, PDBEntry[] pe,
235           SequenceI[][] seqs)
236   {
237     openNewJmol(ap, alignAdded, pe, seqs);
238   }
239
240
241   void initJmol(String command)
242   {
243     jmb.setFinishedInit(false);
244     renderPanel = new RenderPanel();
245     // TODO: consider waiting until the structure/view is fully loaded before
246     // displaying
247     this.getContentPane().add(renderPanel, java.awt.BorderLayout.CENTER);
248     jalview.gui.Desktop.addInternalFrame(this, jmb.getViewerTitle(),
249             getBounds().width, getBounds().height);
250     if (scriptWindow == null)
251     {
252       BorderLayout bl = new BorderLayout();
253       bl.setHgap(0);
254       bl.setVgap(0);
255       scriptWindow = new JPanel(bl);
256       scriptWindow.setVisible(false);
257     }
258
259     jmb.allocateViewer(renderPanel, true, "", null, null, "", scriptWindow,
260             null);
261     // jmb.newJmolPopup("Jmol");
262     if (command == null)
263     {
264       command = "";
265     }
266     jmb.executeCommand(new StructureCommand(command), false);
267     jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false);
268     jmb.setFinishedInit(true);
269   }
270
271   @Override
272   public void run()
273   {
274     _started = true;
275     try
276     {
277       List<String> files = fetchPdbFiles();
278       if (files.size() > 0)
279       {
280         showFilesInViewer(files);
281       }
282     } finally
283     {
284       _started = false;
285       worker = null;
286     }
287   }
288
289   /**
290    * Either adds the given files to a structure viewer or opens a new viewer to
291    * show them
292    * 
293    * @param files
294    *          list of absolute paths to structure files
295    */
296   void showFilesInViewer(List<String> files)
297   {
298     long lastnotify = jmb.getLoadNotifiesHandled();
299     StringBuilder fileList = new StringBuilder();
300     for (String s : files)
301     {
302       fileList.append(SPACE).append(QUOTE)
303               .append(Platform.escapeBackslashes(s)).append(QUOTE);
304     }
305     String filesString = fileList.toString();
306
307     if (!addingStructures)
308     {
309       try
310       {
311         initJmol("load FILES " + filesString);
312       } catch (OutOfMemoryError oomerror)
313       {
314         new OOMWarning("When trying to open the Jmol viewer!", oomerror);
315         Cache.log.debug("File locations are " + filesString);
316       } catch (Exception ex)
317       {
318         Cache.log.error("Couldn't open Jmol viewer!", ex);
319         ex.printStackTrace();
320         return;
321       }
322     }
323     else
324     {
325       StringBuilder cmd = new StringBuilder();
326       cmd.append("loadingJalviewdata=true\nload APPEND ");
327       cmd.append(filesString);
328       cmd.append("\nloadingJalviewdata=null");
329       final StructureCommand command = new StructureCommand(cmd.toString());
330       lastnotify = jmb.getLoadNotifiesHandled();
331
332       try
333       {
334         jmb.executeCommand(command, false);
335       } catch (OutOfMemoryError oomerror)
336       {
337         new OOMWarning("When trying to add structures to the Jmol viewer!",
338                 oomerror);
339         Cache.log.debug("File locations are " + filesString);
340         return;
341       } catch (Exception ex)
342       {
343         Cache.log.error("Couldn't add files to Jmol viewer!", ex);
344         ex.printStackTrace();
345         return;
346       }
347     }
348
349     // need to wait around until script has finished
350     int waitMax = JMOL_LOAD_TIMEOUT;
351     int waitFor = 35;
352     int waitTotal = 0;
353     while (addingStructures ? lastnotify >= jmb.getLoadNotifiesHandled()
354             : !(jmb.isFinishedInit() && jmb.getStructureFiles() != null
355                     && jmb.getStructureFiles().length == files.size()))
356     {
357       try
358       {
359         Cache.log.debug("Waiting around for jmb notify.");
360         waitTotal += waitFor;
361
362         // Thread.sleep() throws an exception in JS
363         Thread.sleep(waitFor);
364       } catch (Exception e)
365       {
366       }
367       if (waitTotal > waitMax)
368       {
369         System.err.println("Timed out waiting for Jmol to load files after "
370                 + waitTotal + "ms");
371         // System.err.println("finished: " + jmb.isFinishedInit()
372         // + "; loaded: " + Arrays.toString(jmb.getPdbFile())
373         // + "; files: " + files.toString());
374         jmb.getStructureFiles();
375         break;
376       }
377     }
378
379     // refresh the sequence colours for the new structure(s)
380     for (AlignmentViewPanel ap : _colourwith)
381     {
382       jmb.updateColours(ap);
383     }
384     // do superposition if asked to
385     if (alignAddedStructures)
386     {
387       alignAddedStructures();
388     }
389     addingStructures = false;
390   }
391
392   /**
393    * Queues a thread to align structures with Jalview alignments
394    */
395   void alignAddedStructures()
396   {
397     javax.swing.SwingUtilities.invokeLater(new Runnable()
398     {
399       @Override
400       public void run()
401       {
402         if (jmb.jmolViewer.isScriptExecuting())
403         {
404           SwingUtilities.invokeLater(this);
405           try
406           {
407             Thread.sleep(5);
408           } catch (InterruptedException q)
409           {
410           }
411           return;
412         }
413         else
414         {
415           alignStructsWithAllAlignPanels();
416         }
417       }
418     });
419
420   }
421
422   /**
423    * Retrieves and saves as file any modelled PDB entries for which we do not
424    * already have a file saved. Returns a list of absolute paths to structure
425    * files which were either retrieved, or already stored but not modelled in
426    * the structure viewer (i.e. files to add to the viewer display).
427    * 
428    * @return
429    */
430   List<String> fetchPdbFiles()
431   {
432     // todo - record which pdbids were successfully imported.
433     StringBuilder errormsgs = new StringBuilder();
434
435     List<String> files = new ArrayList<>();
436     String pdbid = "";
437     try
438     {
439       String[] filesInViewer = jmb.getStructureFiles();
440       // TODO: replace with reference fetching/transfer code (validate PDBentry
441       // as a DBRef?)
442       Pdb pdbclient = new Pdb();
443       EBIAlfaFold afclient = new EBIAlfaFold();
444       
445       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
446       {
447         String file = jmb.getPdbEntry(pi).getFile();
448         if (file == null)
449         {
450           // todo: extract block as method and pull up (also ChimeraViewFrame)
451           // retrieve the pdb and store it locally
452           AlignmentI pdbseq = null;
453           PDBEntry strucEntry = jmb.getPdbEntry(pi);
454           pdbid = strucEntry.getId();
455           long hdl = pdbid.hashCode() - System.currentTimeMillis();
456           setProgressMessage(MessageManager
457                   .formatMessage("status.fetching_pdb", new String[]
458                   { pdbid }), hdl);
459           try
460           {
461             if (afclient.isValidReference(pdbid))
462             {
463               pdbseq = afclient.getSequenceRecords(pdbid);
464             } else {
465               if (strucEntry.hasRetrievalUrl())
466               {
467                 File tmpFile = File.createTempFile(pdbid, "cif");
468                 String fromUrl = strucEntry.getRetrievalUrl();
469                 UrlDownloadClient.download(fromUrl, tmpFile);
470                 
471                 // may not need this check ?
472                 file = tmpFile.getAbsolutePath();
473                 if (file != null)
474                 {
475                   pdbseq = EBIAlfaFold.importDownloadedStructureFromUrl(fromUrl,tmpFile,pdbid,null,null,null);
476                 }
477               } else {
478                 pdbseq = pdbclient.getSequenceRecords(pdbid);
479               }
480             }
481           } catch (OutOfMemoryError oomerror)
482           {
483             new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
484           } catch (Exception ex)
485           {
486             ex.printStackTrace();
487             errormsgs.append("'").append(pdbid).append("'");
488           } finally
489           {
490             setProgressMessage(
491                     MessageManager.getString("label.state_completed"), hdl);
492           }
493           if (pdbseq != null)
494           {
495             // just transfer the file name from the first sequence's first
496             // PDBEntry
497             file = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
498                     .elementAt(0).getFile()).getAbsolutePath();
499             jmb.getPdbEntry(pi).setFile(file);
500             files.add(file);
501           }
502           else
503           {
504             errormsgs.append("'").append(pdbid).append("' ");
505           }
506         }
507         else
508         {
509           if (filesInViewer != null && filesInViewer.length > 0)
510           {
511             addingStructures = true; // already files loaded.
512             for (int c = 0; c < filesInViewer.length; c++)
513             {
514               if (Platform.pathEquals(filesInViewer[c], file))
515               {
516                 file = null;
517                 break;
518               }
519             }
520           }
521           if (file != null)
522           {
523             files.add(file);
524           }
525         }
526       }
527     } catch (OutOfMemoryError oomerror)
528     {
529       new OOMWarning("Retrieving PDB files: " + pdbid, oomerror);
530     } catch (Exception ex)
531     {
532       ex.printStackTrace();
533       errormsgs.append("When retrieving pdbfiles : current was: '")
534               .append(pdbid).append("'");
535     }
536     if (errormsgs.length() > 0)
537     {
538       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
539               MessageManager.formatMessage(
540                       "label.pdb_entries_couldnt_be_retrieved", new String[]
541                       { errormsgs.toString() }),
542               MessageManager.getString("label.couldnt_load_file"),
543               JvOptionPane.ERROR_MESSAGE);
544     }
545     return files;
546   }
547
548   /**
549    * Outputs the Jmol viewer image as an image file, after prompting the user to
550    * choose a file and (for EPS) choice of Text or Lineart character rendering
551    * (unless a preference for this is set)
552    * 
553    * @param type
554    */
555   @Override
556   public void makePDBImage(ImageMaker.TYPE type)
557   {
558     int width = getWidth();
559     int height = getHeight();
560     ImageWriterI writer = new ImageWriterI()
561     {
562       @Override
563       public void exportImage(Graphics g) throws Exception
564       {
565         jmb.jmolViewer.renderScreenImage(g, width, height);
566       }
567     };
568     String view = MessageManager.getString("action.view").toLowerCase();
569     ImageExporter exporter = new ImageExporter(writer,
570             getProgressIndicator(), type, getTitle());
571     exporter.doExport(null, this, width, height, view);
572   }
573
574   @Override
575   public void showHelp_actionPerformed()
576   {
577     try
578     {
579       BrowserLauncher // BH 2018
580               .openURL("http://wiki.jmol.org");//http://jmol.sourceforge.net/docs/JmolUserGuide/");
581     } catch (Exception ex)
582     {
583       System.err.println("Show Jmol help failed with: " + ex.getMessage());
584     }
585   }
586
587   @Override
588   public void showConsole(boolean showConsole)
589   {
590     if (showConsole)
591     {
592       if (splitPane == null)
593       {
594         splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
595         splitPane.setTopComponent(renderPanel);
596         splitPane.setBottomComponent(scriptWindow);
597         this.getContentPane().add(splitPane, BorderLayout.CENTER);
598         splitPane.setDividerLocation(getHeight() - 200);
599         scriptWindow.setVisible(true);
600         scriptWindow.validate();
601         splitPane.validate();
602       }
603
604     }
605     else
606     {
607       if (splitPane != null)
608       {
609         splitPane.setVisible(false);
610       }
611
612       splitPane = null;
613
614       this.getContentPane().add(renderPanel, BorderLayout.CENTER);
615     }
616
617     validate();
618   }
619
620   class RenderPanel extends JPanel
621   {
622     final Dimension currentSize = new Dimension();
623
624     @Override
625     public void paintComponent(Graphics g)
626     {
627       getSize(currentSize);
628
629       if (jmb != null && jmb.hasFileLoadingError())
630       {
631         g.setColor(Color.black);
632         g.fillRect(0, 0, currentSize.width, currentSize.height);
633         g.setColor(Color.white);
634         g.setFont(new Font("Verdana", Font.BOLD, 14));
635         g.drawString(MessageManager.getString("label.error_loading_file")
636                 + "...", 20, currentSize.height / 2);
637         StringBuffer sb = new StringBuffer();
638         int lines = 0;
639         for (int e = 0; e < jmb.getPdbCount(); e++)
640         {
641           sb.append(jmb.getPdbEntry(e).getId());
642           if (e < jmb.getPdbCount() - 1)
643           {
644             sb.append(",");
645           }
646
647           if (e == jmb.getPdbCount() - 1 || sb.length() > 20)
648           {
649             lines++;
650             g.drawString(sb.toString(), 20, currentSize.height / 2
651                     - lines * g.getFontMetrics().getHeight());
652           }
653         }
654       }
655       else if (jmb == null || jmb.jmolViewer == null || !jmb.isFinishedInit())
656       {
657         g.setColor(Color.black);
658         g.fillRect(0, 0, currentSize.width, currentSize.height);
659         g.setColor(Color.white);
660         g.setFont(new Font("Verdana", Font.BOLD, 14));
661         g.drawString(MessageManager.getString("label.retrieving_pdb_data"),
662                 20, currentSize.height / 2);
663       }
664       else
665       {
666         jmb.jmolViewer.renderScreenImage(g, currentSize.width,
667                 currentSize.height);
668       }
669     }
670   }
671
672   @Override
673   public AAStructureBindingModel getBinding()
674   {
675     return this.jmb;
676   }
677
678   @Override
679   public ViewerType getViewerType()
680   {
681     return ViewerType.JMOL;
682   }
683
684   @Override
685   protected String getViewerName()
686   {
687     return "Jmol";
688   }
689 }