67eddcac4a21b8c0a0e25564c74374e12d70011b
[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 jalview.bin.Cache;
24 import jalview.datamodel.AlignmentI;
25 import jalview.datamodel.PDBEntry;
26 import jalview.datamodel.SequenceI;
27 import jalview.ext.rbvi.chimera.ChimeraCommands;
28 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
29 import jalview.gui.StructureViewer.ViewerType;
30 import jalview.io.DataSourceType;
31 import jalview.io.StructureFile;
32 import jalview.structures.models.AAStructureBindingModel;
33 import jalview.util.BrowserLauncher;
34 import jalview.util.MessageManager;
35 import jalview.util.Platform;
36 import jalview.ws.dbsources.Pdb;
37
38 import java.awt.event.ActionEvent;
39 import java.awt.event.ActionListener;
40 import java.awt.event.MouseAdapter;
41 import java.awt.event.MouseEvent;
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.List;
49 import java.util.Random;
50
51 import javax.swing.JCheckBoxMenuItem;
52 import javax.swing.JInternalFrame;
53 import javax.swing.JMenu;
54 import javax.swing.JMenuItem;
55 import javax.swing.event.InternalFrameAdapter;
56 import javax.swing.event.InternalFrameEvent;
57
58 /**
59  * GUI elements for handling an external chimera display
60  * 
61  * @author jprocter
62  *
63  */
64 public class ChimeraViewFrame extends StructureViewerBase
65 {
66   private JalviewChimeraBinding jmb;
67
68   private IProgressIndicator progressBar = null;
69
70   /*
71    * Path to Chimera session file. This is set when an open Jalview/Chimera
72    * session is saved, or on restore from a Jalview project (if it holds the
73    * filename of any saved Chimera sessions).
74    */
75   private String chimeraSessionFile = null;
76
77   private Random random = new Random();
78
79   /**
80    * Initialise menu options.
81    */
82   @Override
83   protected void initMenus()
84   {
85     super.initMenus();
86
87     viewerActionMenu.setText(MessageManager.getString("label.chimera"));
88
89     viewerColour.setText(MessageManager
90             .getString("label.colour_with_chimera"));
91     viewerColour.setToolTipText(MessageManager
92             .getString("label.let_chimera_manage_structure_colours"));
93
94     helpItem.setText(MessageManager.getString("label.chimera_help"));
95     savemenu.setVisible(false); // not yet implemented
96     viewMenu.add(fitToWindow);
97
98     JMenuItem writeFeatures = new JMenuItem(
99             MessageManager.getString("label.create_chimera_attributes"));
100     writeFeatures.setToolTipText(MessageManager
101             .getString("label.create_chimera_attributes_tip"));
102     writeFeatures.addActionListener(new ActionListener()
103     {
104       @Override
105       public void actionPerformed(ActionEvent e)
106       {
107         sendFeaturesToChimera();
108       }
109     });
110     viewerActionMenu.add(writeFeatures);
111
112     final JMenu fetchAttributes = new JMenu(
113             MessageManager.getString("label.fetch_chimera_attributes"));
114     fetchAttributes.setToolTipText(MessageManager
115             .getString("label.fetch_chimera_attributes_tip"));
116     fetchAttributes.addMouseListener(new MouseAdapter()
117     {
118
119       @Override
120       public void mouseEntered(MouseEvent e)
121       {
122         buildAttributesMenu(fetchAttributes);
123       }
124     });
125     viewerActionMenu.add(fetchAttributes);
126
127   }
128
129   /**
130    * Query Chimera for its residue attribute names and add them as items off the
131    * attributes menu
132    * 
133    * @param attributesMenu
134    */
135   protected void buildAttributesMenu(JMenu attributesMenu)
136   {
137     List<String> atts = jmb.sendChimeraCommand("list resattr", true);
138     if (atts == null)
139     {
140       return;
141     }
142     attributesMenu.removeAll();
143     Collections.sort(atts);
144     for (String att : atts)
145     {
146       final String attName = att.split(" ")[1];
147
148       /*
149        * ignore 'jv_*' attributes, as these are Jalview features that have
150        * been transferred to residue attributes in Chimera!
151        */
152       if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX))
153       {
154         JMenuItem menuItem = new JMenuItem(attName);
155         menuItem.addActionListener(new ActionListener()
156         {
157           @Override
158           public void actionPerformed(ActionEvent e)
159           {
160             getChimeraAttributes(attName);
161           }
162         });
163         attributesMenu.add(menuItem);
164       }
165     }
166   }
167
168   /**
169    * Read residues in Chimera with the given attribute name, and set as features
170    * on the corresponding sequence positions (if any)
171    * 
172    * @param attName
173    */
174   protected void getChimeraAttributes(String attName)
175   {
176     jmb.copyStructureAttributesToFeatures(attName, getAlignmentPanel());
177   }
178
179   /**
180    * Send a command to Chimera to create residue attributes for Jalview features
181    * <p>
182    * The syntax is: setattr r <attName> <attValue> <atomSpec>
183    * <p>
184    * For example: setattr r jv:chain "Ferredoxin-1, Chloroplastic" #0:94.A
185    */
186   protected void sendFeaturesToChimera()
187   {
188     jmb.sendFeaturesToViewer(getAlignmentPanel());
189   }
190
191   /**
192    * add a single PDB structure to a new or existing Chimera view
193    * 
194    * @param pdbentry
195    * @param seq
196    * @param chains
197    * @param ap
198    */
199   public ChimeraViewFrame(PDBEntry pdbentry, SequenceI[] seq,
200           String[] chains, final AlignmentPanel ap)
201   {
202     this();
203     String pdbId = pdbentry.getId();
204
205     /*
206      * If the PDB file is already loaded, the user may just choose to add to an
207      * existing viewer (or cancel)
208      */
209     if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
210     {
211       return;
212     }
213
214     /*
215      * Check if there are other Chimera views involving this alignment and give
216      * user the option to add and align this molecule to one of them (or cancel)
217      */
218     if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
219     {
220       return;
221     }
222
223     /*
224      * If the options above are declined or do not apply, show the structure in
225      * a new viewer
226      */
227     openNewChimera(ap, new PDBEntry[] { pdbentry },
228             new SequenceI[][] { seq });
229   }
230
231   /**
232    * Create a helper to manage progress bar display
233    */
234   protected void createProgressBar()
235   {
236     if (progressBar == null)
237     {
238       progressBar = new ProgressBar(statusPanel, statusBar);
239     }
240   }
241
242   private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
243           SequenceI[][] seqs)
244   {
245     createProgressBar();
246     jmb = new JalviewChimeraBindingModel(this,
247             ap.getStructureSelectionManager(), pdbentrys, seqs, null);
248     addAlignmentPanel(ap);
249     useAlignmentPanelForColourbyseq(ap);
250
251     if (pdbentrys.length > 1)
252     {
253       alignAddedStructures = true;
254       useAlignmentPanelForSuperposition(ap);
255     }
256     jmb.setColourBySequence(true);
257     setSize(400, 400); // probably should be a configurable/dynamic default here
258     initMenus();
259
260     addingStructures = false;
261     worker = new Thread(this);
262     worker.start();
263
264     this.addInternalFrameListener(new InternalFrameAdapter()
265     {
266       @Override
267       public void internalFrameClosing(InternalFrameEvent internalFrameEvent)
268       {
269         closeViewer(false);
270       }
271     });
272
273   }
274
275   /**
276    * Create a new viewer from saved session state data including Chimera session
277    * file
278    * 
279    * @param chimeraSessionFile
280    * @param alignPanel
281    * @param pdbArray
282    * @param seqsArray
283    * @param colourByChimera
284    * @param colourBySequence
285    * @param newViewId
286    */
287   public ChimeraViewFrame(String chimeraSessionFile,
288           AlignmentPanel alignPanel, PDBEntry[] pdbArray,
289           SequenceI[][] seqsArray, boolean colourByChimera,
290           boolean colourBySequence, String newViewId)
291   {
292     this();
293     setViewId(newViewId);
294     this.chimeraSessionFile = chimeraSessionFile;
295     openNewChimera(alignPanel, pdbArray, seqsArray);
296     if (colourByChimera)
297     {
298       jmb.setColourBySequence(false);
299       seqColour.setSelected(false);
300       viewerColour.setSelected(true);
301     }
302     else if (colourBySequence)
303     {
304       jmb.setColourBySequence(true);
305       seqColour.setSelected(true);
306       viewerColour.setSelected(false);
307     }
308   }
309
310   /**
311    * create a new viewer containing several structures superimposed using the
312    * given alignPanel.
313    * 
314    * @param pe
315    * @param seqs
316    * @param ap
317    */
318   public ChimeraViewFrame(PDBEntry[] pe, SequenceI[][] seqs,
319           AlignmentPanel ap)
320   {
321     this();
322     openNewChimera(ap, pe, seqs);
323   }
324
325   /**
326    * Default constructor
327    */
328   public ChimeraViewFrame()
329   {
330     super();
331
332     /*
333      * closeViewer will decide whether or not to close this frame
334      * depending on whether user chooses to Cancel or not
335      */
336     setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
337   }
338
339   /**
340    * Returns a list of any Chimera viewers in the desktop. The list is
341    * restricted to those linked to the given alignment panel if it is not null.
342    */
343   @Override
344   protected List<StructureViewerBase> getViewersFor(AlignmentPanel ap)
345   {
346     List<StructureViewerBase> result = new ArrayList<StructureViewerBase>();
347     JInternalFrame[] frames = Desktop.instance.getAllFrames();
348
349     for (JInternalFrame frame : frames)
350     {
351       if (frame instanceof ChimeraViewFrame)
352       {
353         if (ap == null || ((StructureViewerBase) frame).isLinkedWith(ap))
354         {
355           result.add((StructureViewerBase) frame);
356         }
357       }
358     }
359     return result;
360   }
361
362   /**
363    * Launch Chimera. If we have a chimera session file name, send Chimera the
364    * command to open its saved session file.
365    */
366   void initChimera()
367   {
368     jmb.setFinishedInit(false);
369     Desktop.addInternalFrame(this,
370             jmb.getViewerTitle(getViewerName(), true), getBounds().width,
371             getBounds().height);
372
373     if (!jmb.launchChimera())
374     {
375       JvOptionPane.showMessageDialog(Desktop.desktop,
376               MessageManager.getString("label.chimera_failed"),
377               MessageManager.getString("label.error_loading_file"),
378               JvOptionPane.ERROR_MESSAGE);
379       this.dispose();
380       return;
381     }
382
383     if (this.chimeraSessionFile != null)
384     {
385       boolean opened = jmb.openSession(chimeraSessionFile);
386       if (!opened)
387       {
388         System.err
389                 .println("An error occurred opening Chimera session file "
390                         + chimeraSessionFile);
391       }
392     }
393
394     jmb.startChimeraListener();
395   }
396
397   /**
398    * Show only the selected chain(s) in the viewer
399    */
400   @Override
401   void showSelectedChains()
402   {
403     List<String> toshow = new ArrayList<String>();
404     for (int i = 0; i < chainMenu.getItemCount(); i++)
405     {
406       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
407       {
408         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
409         if (item.isSelected())
410         {
411           toshow.add(item.getText());
412         }
413       }
414     }
415     jmb.showChains(toshow);
416   }
417
418   /**
419    * Close down this instance of Jalview's Chimera viewer, giving the user the
420    * option to close the associated Chimera window (process). They may wish to
421    * keep it open until they have had an opportunity to save any work.
422    * 
423    * @param closeChimera
424    *          if true, close any linked Chimera process; if false, prompt first
425    */
426   @Override
427   public void closeViewer(boolean closeChimera)
428   {
429     if (jmb != null && jmb.isChimeraRunning())
430     {
431       if (!closeChimera)
432       {
433         String prompt = MessageManager.formatMessage(
434                 "label.confirm_close_chimera",
435                         new Object[] { jmb.getViewerTitle(getViewerName(),
436                                 false) });
437         prompt = JvSwingUtils.wrapTooltip(true, prompt);
438         int confirm = JvOptionPane.showConfirmDialog(this, prompt,
439                 MessageManager.getString("label.close_viewer"),
440                 JvOptionPane.YES_NO_CANCEL_OPTION);
441         /*
442          * abort closure if user hits escape or Cancel
443          */
444         if (confirm == JvOptionPane.CANCEL_OPTION
445                 || confirm == JvOptionPane.CLOSED_OPTION)
446         {
447           return;
448         }
449         closeChimera = confirm == JvOptionPane.YES_OPTION;
450       }
451       jmb.closeViewer(closeChimera);
452     }
453     setAlignmentPanel(null);
454     _aps.clear();
455     _alignwith.clear();
456     _colourwith.clear();
457     // TODO: check for memory leaks where instance isn't finalised because jmb
458     // holds a reference to the window
459     jmb = null;
460     dispose();
461   }
462
463   /**
464    * Open any newly added PDB structures in Chimera, having first fetched data
465    * from PDB (if not already saved).
466    */
467   @Override
468   public void run()
469   {
470     _started = true;
471     // todo - record which pdbids were successfully imported.
472     StringBuilder errormsgs = new StringBuilder(128);
473     StringBuilder files = new StringBuilder(128);
474     List<PDBEntry> filePDB = new ArrayList<PDBEntry>();
475     List<Integer> filePDBpos = new ArrayList<Integer>();
476     PDBEntry thePdbEntry = null;
477     StructureFile pdb = null;
478     try
479     {
480       String[] curfiles = jmb.getPdbFile(); // files currently in viewer
481       // TODO: replace with reference fetching/transfer code (validate PDBentry
482       // as a DBRef?)
483       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
484       {
485         String file = null;
486         thePdbEntry = jmb.getPdbEntry(pi);
487         if (thePdbEntry.getFile() == null)
488         {
489           /*
490            * Retrieve PDB data, save to file, attach to PDBEntry
491            */
492           file = fetchPdbFile(thePdbEntry);
493           if (file == null)
494           {
495             errormsgs.append("'" + thePdbEntry.getId() + "' ");
496           }
497         }
498         else
499         {
500           /*
501            * Got file already - ignore if already loaded in Chimera.
502            */
503           file = new File(thePdbEntry.getFile()).getAbsoluteFile()
504                   .getPath();
505           if (curfiles != null && curfiles.length > 0)
506           {
507             addingStructures = true; // already files loaded.
508             for (int c = 0; c < curfiles.length; c++)
509             {
510               if (curfiles[c].equals(file))
511               {
512                 file = null;
513                 break;
514               }
515             }
516           }
517         }
518         if (file != null)
519         {
520           filePDB.add(thePdbEntry);
521           filePDBpos.add(Integer.valueOf(pi));
522           files.append(" \"" + Platform.escapeString(file) + "\"");
523         }
524       }
525     } catch (OutOfMemoryError oomerror)
526     {
527       new OOMWarning("Retrieving PDB files: " + thePdbEntry.getId(),
528               oomerror);
529     } catch (Exception ex)
530     {
531       ex.printStackTrace();
532       errormsgs.append("When retrieving pdbfiles for '"
533               + thePdbEntry.getId() + "'");
534     }
535     if (errormsgs.length() > 0)
536     {
537
538       JvOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
539               .formatMessage("label.pdb_entries_couldnt_be_retrieved",
540                       new Object[] { errormsgs.toString() }),
541               MessageManager.getString("label.couldnt_load_file"),
542               JvOptionPane.ERROR_MESSAGE);
543     }
544
545     if (files.length() > 0)
546     {
547       jmb.setFinishedInit(false);
548       if (!addingStructures)
549       {
550         try
551         {
552           initChimera();
553         } catch (Exception ex)
554         {
555           Cache.log.error("Couldn't open Chimera viewer!", ex);
556         }
557       }
558       int num = -1;
559       for (PDBEntry pe : filePDB)
560       {
561         num++;
562         if (pe.getFile() != null)
563         {
564           try
565           {
566             int pos = filePDBpos.get(num).intValue();
567             long startTime = startProgressBar(getViewerName() + " "
568                     + MessageManager.getString("status.opening_file_for")
569                     + " " + pe.getId());
570             jmb.openFile(pe);
571             jmb.addSequence(pos, jmb.getSequence()[pos]);
572             File fl = new File(pe.getFile());
573             DataSourceType protocol = DataSourceType.URL;
574             try
575             {
576               if (fl.exists())
577               {
578                 protocol = DataSourceType.FILE;
579               }
580             } catch (Throwable e)
581             {
582             } finally
583             {
584               stopProgressBar("", startTime);
585             }
586             // Explicitly map to the filename used by Chimera ;
587             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
588                     jmb.getChains()[pos], pe.getFile(), protocol);
589             stashFoundChains(pdb, pe.getFile());
590           } catch (OutOfMemoryError oomerror)
591           {
592             new OOMWarning(
593                     "When trying to open and map structures from Chimera!",
594                     oomerror);
595           } catch (Exception ex)
596           {
597             Cache.log.error("Couldn't open " + pe.getFile()
598                     + " in Chimera viewer!", ex);
599           } finally
600           {
601             Cache.log.debug("File locations are " + files);
602           }
603         }
604       }
605
606       jmb.refreshGUI();
607       jmb.setFinishedInit(true);
608       jmb.setLoadingFromArchive(false);
609
610       // refresh the sequence colours for the new structure(s)
611       for (AlignmentPanel ap : _colourwith)
612       {
613         jmb.updateColours(ap);
614       }
615       // do superposition if asked to
616       if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
617       {
618         new Thread(new Runnable()
619         {
620           @Override
621           public void run()
622           {
623             alignStructs_withAllAlignPanels();
624           }
625         }).start();
626         alignAddedStructures = false;
627       }
628       addingStructures = false;
629     }
630     _started = false;
631     worker = null;
632   }
633
634   /**
635    * Fetch PDB data and save to a local file. Returns the full path to the file,
636    * or null if fetch fails.
637    * 
638    * @param processingEntry
639    * @return
640    * @throws Exception
641    */
642
643   private void stashFoundChains(StructureFile pdb, String file)
644   {
645     for (int i = 0; i < pdb.getChains().size(); i++)
646     {
647       String chid = new String(pdb.getId() + ":"
648               + pdb.getChains().elementAt(i).id);
649       jmb.getChainNames().add(chid);
650       jmb.getChainFile().put(chid, file);
651     }
652   }
653   private String fetchPdbFile(PDBEntry processingEntry) throws Exception
654   {
655     // FIXME: this is duplicated code with Jmol frame ?
656     String filePath = null;
657     Pdb pdbclient = new Pdb();
658     AlignmentI pdbseq = null;
659     String pdbid = processingEntry.getId();
660     long handle = System.currentTimeMillis()
661             + Thread.currentThread().hashCode();
662
663     /*
664      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
665      */
666     String msg = MessageManager.formatMessage("status.fetching_pdb",
667             new Object[] { pdbid });
668     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
669     // long hdl = startProgressBar(MessageManager.formatMessage(
670     // "status.fetching_pdb", new Object[]
671     // { pdbid }));
672     try
673     {
674       pdbseq = pdbclient.getSequenceRecords(pdbid);
675     } catch (OutOfMemoryError oomerror)
676     {
677       new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
678     } finally
679     {
680       msg = pdbid + " " + MessageManager.getString("label.state_completed");
681       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
682       // stopProgressBar(msg, hdl);
683     }
684     /*
685      * If PDB data were saved and are not invalid (empty alignment), return the
686      * file path.
687      */
688     if (pdbseq != null && pdbseq.getHeight() > 0)
689     {
690       // just use the file name from the first sequence's first PDBEntry
691       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
692               .elementAt(0).getFile()).getAbsolutePath();
693       processingEntry.setFile(filePath);
694     }
695     return filePath;
696   }
697
698   /**
699    * Convenience method to update the progress bar if there is one. Be sure to
700    * call stopProgressBar with the returned handle to remove the message.
701    * 
702    * @param msg
703    * @param handle
704    */
705   public long startProgressBar(String msg)
706   {
707     // TODO would rather have startProgress/stopProgress as the
708     // IProgressIndicator interface
709     long tm = random.nextLong();
710     if (progressBar != null)
711     {
712       progressBar.setProgressBar(msg, tm);
713     }
714     return tm;
715   }
716
717   /**
718    * End the progress bar with the specified handle, leaving a message (if not
719    * null) on the status bar
720    * 
721    * @param msg
722    * @param handle
723    */
724   public void stopProgressBar(String msg, long handle)
725   {
726     if (progressBar != null)
727     {
728       progressBar.setProgressBar(msg, handle);
729     }
730   }
731
732   @Override
733   public void eps_actionPerformed(ActionEvent e)
734   {
735     throw new Error(
736             MessageManager
737                     .getString("error.eps_generation_not_implemented"));
738   }
739
740   @Override
741   public void png_actionPerformed(ActionEvent e)
742   {
743     throw new Error(
744             MessageManager
745                     .getString("error.png_generation_not_implemented"));
746   }
747
748   @Override
749   public void showHelp_actionPerformed(ActionEvent actionEvent)
750   {
751     try
752     {
753       BrowserLauncher
754               .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
755     } catch (IOException ex)
756     {
757     }
758   }
759
760   @Override
761   public AAStructureBindingModel getBinding()
762   {
763     return jmb;
764   }
765
766   /**
767    * Ask Chimera to save its session to the designated file path, or to a
768    * temporary file if the path is null. Returns the file path if successful,
769    * else null.
770    * 
771    * @param filepath
772    * @see getStateInfo
773    */
774   protected String saveSession(String filepath)
775   {
776     String pathUsed = filepath;
777     try
778     {
779       if (pathUsed == null)
780       {
781         File tempFile = File.createTempFile("chimera", ".py");
782         tempFile.deleteOnExit();
783         pathUsed = tempFile.getPath();
784       }
785       boolean result = jmb.saveSession(pathUsed);
786       if (result)
787       {
788         this.chimeraSessionFile = pathUsed;
789         return pathUsed;
790       }
791     } catch (IOException e)
792     {
793     }
794     return null;
795   }
796
797   /**
798    * Returns a string representing the state of the Chimera session. This is
799    * done by requesting Chimera to save its session to a temporary file, then
800    * reading the file contents. Returns an empty string on any error.
801    */
802   @Override
803   public String getStateInfo()
804   {
805     String sessionFile = saveSession(null);
806     if (sessionFile == null)
807     {
808       return "";
809     }
810     InputStream is = null;
811     try
812     {
813       File f = new File(sessionFile);
814       byte[] bytes = new byte[(int) f.length()];
815       is = new FileInputStream(sessionFile);
816       is.read(bytes);
817       return new String(bytes);
818     } catch (IOException e)
819     {
820       return "";
821     } finally
822     {
823       if (is != null)
824       {
825         try
826         {
827           is.close();
828         } catch (IOException e)
829         {
830           // ignore
831         }
832       }
833     }
834   }
835
836   @Override
837   protected void fitToWindow_actionPerformed()
838   {
839     jmb.focusView();
840   }
841
842   @Override
843   public ViewerType getViewerType()
844   {
845     return ViewerType.CHIMERA;
846   }
847
848   @Override
849   protected String getViewerName()
850   {
851     return "Chimera";
852   }
853
854   /**
855    * Override superclass method to make the 'Chimera' menu always visible, but
856    * 'Superpose with...' only enabled if there is more than one structure shown
857    */
858   @Override
859   public void updateTitleAndMenus()
860   {
861     super.updateTitleAndMenus();
862     viewerActionMenu.setVisible(true);
863     viewSelectionMenu.setEnabled(false);
864     if (getBinding().getPdbFile().length > 1
865             && getBinding().getSequence().length > 1)
866     {
867       viewSelectionMenu.setEnabled(true);
868     }
869   }
870 }