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