JAL-3619 fix up error messages and try to bail quietly if the viewer doesn’t open
[jalview.git] / src / jalview / gui / ChimeraViewFrame.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.event.ActionEvent;
24 import java.awt.event.ActionListener;
25 import java.awt.event.MouseAdapter;
26 import java.awt.event.MouseEvent;
27 import java.io.File;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Map;
32
33 import javax.swing.JInternalFrame;
34 import javax.swing.JMenu;
35 import javax.swing.JMenuItem;
36 import javax.swing.event.InternalFrameAdapter;
37 import javax.swing.event.InternalFrameEvent;
38
39 import jalview.api.AlignmentViewPanel;
40 import jalview.api.FeatureRenderer;
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.ext.rbvi.chimera.JalviewChimeraBinding;
47 import jalview.gui.StructureViewer.ViewerType;
48 import jalview.io.DataSourceType;
49 import jalview.io.StructureFile;
50 import jalview.structures.models.AAStructureBindingModel;
51 import jalview.util.ImageMaker.TYPE;
52 import jalview.util.MessageManager;
53 import jalview.util.Platform;
54
55 /**
56  * GUI elements for handling an external chimera display
57  * 
58  * @author jprocter
59  *
60  */
61 public class ChimeraViewFrame extends StructureViewerBase
62 {
63   private JalviewChimeraBinding jmb;
64
65   /*
66    * Path to Chimera session file. This is set when an open Jalview/Chimera
67    * session is saved, or on restore from a Jalview project (if it holds the
68    * filename of any saved Chimera sessions).
69    */
70   private String chimeraSessionFile = null;
71
72   private int myWidth = 500;
73
74   private int myHeight = 150;
75
76   private JMenuItem writeFeatures=null;
77
78   private JMenu fetchAttributes=null;
79 /**
80    * Initialise menu options.
81    */
82   @Override
83   protected void initMenus()
84   {
85     super.initMenus();
86
87     savemenu.setVisible(false); // not yet implemented
88     viewMenu.add(fitToWindow);
89
90     writeFeatures = new JMenuItem(
91             MessageManager.getString("label.create_viewer_attributes"));
92     writeFeatures.setToolTipText(MessageManager
93             .getString("label.create_viewer_attributes_tip"));
94     writeFeatures.addActionListener(new ActionListener()
95     {
96       @Override
97       public void actionPerformed(ActionEvent e)
98       {
99         sendFeaturesToChimera();
100       }
101     });
102     viewerActionMenu.add(writeFeatures);
103
104     fetchAttributes = new JMenu(
105             MessageManager.formatMessage("label.fetch_viewer_attributes",getViewerName()));
106     fetchAttributes.setToolTipText(
107             MessageManager.formatMessage("label.fetch_viewer_attributes_tip",getViewerName()));
108     fetchAttributes.addMouseListener(new MouseAdapter()
109     {
110
111       @Override
112       public void mouseEntered(MouseEvent e)
113       {
114         buildAttributesMenu(fetchAttributes);
115       }
116     });
117     viewerActionMenu.add(fetchAttributes);
118   }
119   @Override
120   protected void buildActionMenu()
121   {
122     super.buildActionMenu();
123     // add these back in after menu is refreshed
124     viewerActionMenu.add(writeFeatures);
125     viewerActionMenu.add(fetchAttributes);
126     
127   };
128   /**
129    * Query the structure viewer for its residue attribute names and add them as
130    * items off the attributes menu
131    * 
132    * @param attributesMenu
133    */
134   protected void buildAttributesMenu(JMenu attributesMenu)
135   {
136     List<String> atts = jmb.getChimeraAttributes();
137     attributesMenu.removeAll();
138     Collections.sort(atts);
139     for (String attName : atts)
140     {
141       JMenuItem menuItem = new JMenuItem(attName);
142       menuItem.addActionListener(new ActionListener()
143       {
144         @Override
145         public void actionPerformed(ActionEvent e)
146         {
147           if (getBinding().copyStructureAttributesToFeatures(attName,
148                   getAlignmentPanel()) > 0)
149           {
150             getAlignmentPanel().getFeatureRenderer().featuresAdded();
151           }
152         }
153       });
154       attributesMenu.add(menuItem);
155     }
156   }
157
158   /**
159    * Sends command(s) to the structure viewer to create residue attributes for
160    * visible Jalview features
161    */
162   protected void sendFeaturesToChimera()
163   {
164     // todo pull up?
165     int count = jmb.sendFeaturesToViewer(getAlignmentPanel());
166     statusBar.setText(
167             MessageManager.formatMessage("label.attributes_set", count, getViewerName()));
168   }
169
170   /**
171    * open a single PDB structure in a new Chimera view
172    * 
173    * @param pdbentry
174    * @param seq
175    * @param chains
176    * @param ap
177    */
178   public ChimeraViewFrame(PDBEntry pdbentry, SequenceI[] seq,
179           String[] chains, final AlignmentPanel ap)
180   {
181     this();
182
183     openNewChimera(ap, new PDBEntry[] { pdbentry },
184             new SequenceI[][]
185             { seq });
186   }
187
188   /**
189    * Create a helper to manage progress bar display
190    */
191   protected void createProgressBar()
192   {
193     if (getProgressIndicator() == null)
194     {
195       setProgressIndicator(new ProgressBar(statusPanel, statusBar));
196     }
197   }
198
199   private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
200           SequenceI[][] seqs)
201   {
202     createProgressBar();
203     jmb = newBindingModel(ap, pdbentrys, seqs);
204     addAlignmentPanel(ap);
205     useAlignmentPanelForColourbyseq(ap);
206
207     if (pdbentrys.length > 1)
208     {
209       useAlignmentPanelForSuperposition(ap);
210     }
211     jmb.setColourBySequence(true);
212     setSize(myWidth, myHeight);
213     initMenus();
214
215     addingStructures = false;
216     worker = new Thread(this);
217     worker.start();
218
219     this.addInternalFrameListener(new InternalFrameAdapter()
220     {
221       @Override
222       public void internalFrameClosing(
223               InternalFrameEvent internalFrameEvent)
224       {
225         closeViewer(false);
226       }
227     });
228
229   }
230
231   protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
232           PDBEntry[] pdbentrys, SequenceI[][] seqs)
233   {
234     return new JalviewChimeraBindingModel(this,
235             ap.getStructureSelectionManager(), pdbentrys, seqs, null);
236   }
237
238   /**
239    * Create a new viewer from saved session state data including Chimera session
240    * file
241    * 
242    * @param chimeraSessionFile
243    * @param alignPanel
244    * @param pdbArray
245    * @param seqsArray
246    * @param colourByChimera
247    * @param colourBySequence
248    * @param newViewId
249    */
250   public ChimeraViewFrame(StructureViewerModel viewerData,
251           AlignmentPanel alignPanel, String sessionFile, String vid)
252   {
253     this();
254     setViewId(vid);
255     this.chimeraSessionFile = sessionFile;
256     Map<File, StructureData> pdbData = viewerData.getFileData();
257     PDBEntry[] pdbArray = new PDBEntry[pdbData.size()];
258     SequenceI[][] seqsArray = new SequenceI[pdbData.size()][];
259     int i = 0;
260     for (StructureData data : pdbData.values())
261     {
262       PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
263               PDBEntry.Type.PDB, data.getFilePath());
264       pdbArray[i] = pdbentry;
265       List<SequenceI> sequencesForPdb = data.getSeqList();
266       seqsArray[i] = sequencesForPdb
267               .toArray(new SequenceI[sequencesForPdb.size()]);
268       i++;
269     }
270     openNewChimera(alignPanel, pdbArray, seqsArray);
271     if (viewerData.isColourByViewer())
272     {
273       jmb.setColourBySequence(false);
274       seqColour.setSelected(false);
275       viewerColour.setSelected(true);
276     }
277     else if (viewerData.isColourWithAlignPanel())
278     {
279       jmb.setColourBySequence(true);
280       seqColour.setSelected(true);
281       viewerColour.setSelected(false);
282     }
283   }
284
285   /**
286    * create a new viewer containing several structures, optionally superimposed
287    * using the given alignPanel.
288    * 
289    * @param pe
290    * @param seqs
291    * @param ap
292    */
293   public ChimeraViewFrame(PDBEntry[] pe, boolean alignAdded,
294           SequenceI[][] seqs,
295           AlignmentPanel ap)
296   {
297     this();
298     setAlignAddedStructures(alignAdded);
299     openNewChimera(ap, pe, seqs);
300   }
301
302   /**
303    * Default constructor
304    */
305   public ChimeraViewFrame()
306   {
307     super();
308
309     /*
310      * closeViewer will decide whether or not to close this frame
311      * depending on whether user chooses to Cancel or not
312      */
313     setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
314   }
315
316   /**
317    * Launch Chimera. If we have a chimera session file name, send Chimera the
318    * command to open its saved session file.
319    */
320   void initChimera()
321   {
322     jmb.setFinishedInit(false);
323     Desktop.addInternalFrame(this,
324             jmb.getViewerTitle(getViewerName(), true), getBounds().width,
325             getBounds().height);
326
327     if (!jmb.launchChimera())
328     {
329       JvOptionPane.showMessageDialog(Desktop.desktop,
330               MessageManager.formatMessage("label.open_viewer_failed",
331                       getViewerName()),
332               MessageManager.getString("label.error_loading_file"),
333               JvOptionPane.ERROR_MESSAGE);
334       jmb.closeViewer(true);
335       this.dispose();
336       return;
337     }
338
339     if (this.chimeraSessionFile != null)
340     {
341       boolean opened = jmb.openSession(chimeraSessionFile);
342       if (!opened)
343       {
344         System.err.println("An error occurred opening Chimera session file "
345                 + chimeraSessionFile);
346       }
347     }
348
349     jmb.startChimeraListener();
350   }
351
352   /**
353    * Open any newly added PDB structures in Chimera, having first fetched data
354    * from PDB (if not already saved).
355    */
356   @Override
357   public void run()
358   {
359     _started = true;
360     // todo - record which pdbids were successfully imported.
361     StringBuilder errormsgs = new StringBuilder(128);
362     StringBuilder files = new StringBuilder(128);
363     List<PDBEntry> filePDB = new ArrayList<>();
364     List<Integer> filePDBpos = new ArrayList<>();
365     PDBEntry thePdbEntry = null;
366     StructureFile pdb = null;
367     try
368     {
369       String[] curfiles = jmb.getStructureFiles(); // files currently in viewer
370       // TODO: replace with reference fetching/transfer code (validate PDBentry
371       // as a DBRef?)
372       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
373       {
374         String file = null;
375         thePdbEntry = jmb.getPdbEntry(pi);
376         if (thePdbEntry.getFile() == null)
377         {
378           /*
379            * Retrieve PDB data, save to file, attach to PDBEntry
380            */
381           file = fetchPdbFile(thePdbEntry);
382           if (file == null)
383           {
384             errormsgs.append("'" + thePdbEntry.getId() + "' ");
385           }
386         }
387         else
388         {
389           /*
390            * Got file already - ignore if already loaded in Chimera.
391            */
392           file = new File(thePdbEntry.getFile()).getAbsoluteFile()
393                   .getPath();
394           if (curfiles != null && curfiles.length > 0)
395           {
396             addingStructures = true; // already files loaded.
397             for (int c = 0; c < curfiles.length; c++)
398             {
399               if (curfiles[c].equals(file))
400               {
401                 file = null;
402                 break;
403               }
404             }
405           }
406         }
407         if (file != null)
408         {
409           filePDB.add(thePdbEntry);
410           filePDBpos.add(Integer.valueOf(pi));
411           files.append(" \"" + Platform.escapeBackslashes(file) + "\"");
412         }
413       }
414     } catch (OutOfMemoryError oomerror)
415     {
416       new OOMWarning("Retrieving PDB files: " + thePdbEntry.getId(),
417               oomerror);
418     } catch (Exception ex)
419     {
420       ex.printStackTrace();
421       errormsgs.append(
422               "When retrieving pdbfiles for '" + thePdbEntry.getId() + "'");
423     }
424     if (errormsgs.length() > 0)
425     {
426
427       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
428               MessageManager.formatMessage(
429                       "label.pdb_entries_couldnt_be_retrieved", new Object[]
430                       { errormsgs.toString() }),
431               MessageManager.getString("label.couldnt_load_file"),
432               JvOptionPane.ERROR_MESSAGE);
433     }
434
435     if (files.length() > 0)
436     {
437       jmb.setFinishedInit(false);
438       if (!addingStructures)
439       {
440         try
441         {
442           initChimera();
443         } catch (Exception ex)
444         {
445           Console.error("Couldn't open Chimera viewer!", ex);
446         }
447       }
448       if (!jmb.isViewerRunning())
449       {
450         // nothing to do
451         // TODO: ensure we tidy up JAL-3619
452         return;
453       }
454       int num = -1;
455       for (PDBEntry pe : filePDB)
456       {
457         num++;
458         if (pe.getFile() != null)
459         {
460           try
461           {
462             int pos = filePDBpos.get(num).intValue();
463             long startTime = startProgressBar(getViewerName() + " "
464                     + MessageManager.getString("status.opening_file_for")
465                     + " " + pe.getId());
466             jmb.openFile(pe);
467             jmb.addSequence(pos, jmb.getSequence()[pos]);
468             File fl = new File(pe.getFile());
469             DataSourceType protocol = DataSourceType.URL;
470             try
471             {
472               if (fl.exists())
473               {
474                 protocol = DataSourceType.FILE;
475               }
476             } catch (Throwable e)
477             {
478             } finally
479             {
480               stopProgressBar("", startTime);
481             }
482             // Explicitly map to the filename used by Chimera ;
483
484             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
485                     jmb.getChains()[pos], pe.getFile(), protocol,
486                     getProgressIndicator());
487             jmb.stashFoundChains(pdb, pe.getFile());
488
489           } catch (OutOfMemoryError oomerror)
490           {
491             new OOMWarning(
492                     "When trying to open and map structures from Chimera!",
493                     oomerror);
494           } catch (Exception ex)
495           {
496             Console.error(
497                     "Couldn't open " + pe.getFile() + " in Chimera viewer!",
498                     ex);
499           } finally
500           {
501             Console.debug("File locations are " + files);
502           }
503         }
504       }
505
506       jmb.refreshGUI();
507       jmb.setFinishedInit(true);
508       jmb.setLoadingFromArchive(false);
509
510       /*
511        * ensure that any newly discovered features (e.g. RESNUM)
512        * are notified to the FeatureRenderer (and added to any 
513        * open feature settings dialog)
514        */
515       FeatureRenderer fr = getBinding().getFeatureRenderer(null);
516       if (fr != null)
517       {
518         fr.featuresAdded();
519       }
520
521       // refresh the sequence colours for the new structure(s)
522       for (AlignmentViewPanel ap : _colourwith)
523       {
524         jmb.updateColours(ap);
525       }
526       // do superposition if asked to
527       if (alignAddedStructures)
528       {
529         new Thread(new Runnable()
530         {
531           @Override
532           public void run()
533           {
534             alignStructsWithAllAlignPanels();
535           }
536         }).start();
537       }
538       addingStructures = false;
539     }
540     _started = false;
541     worker = null;
542   }
543
544   @Override
545   public void makePDBImage(TYPE imageType)
546   {
547     throw new UnsupportedOperationException(
548             "Image export for Chimera is not implemented");
549   }
550
551   @Override
552   public AAStructureBindingModel getBinding()
553   {
554     return jmb;
555   }
556
557   @Override
558   public ViewerType getViewerType()
559   {
560     return ViewerType.CHIMERA;
561   }
562
563   @Override
564   protected String getViewerName()
565   {
566     return "Chimera";
567   }
568 }