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