JAL-1743 new Chimera menu option to 'fit to window'
[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.ItemEvent;
26 import java.awt.event.ItemListener;
27 import java.io.BufferedReader;
28 import java.io.File;
29 import java.io.FileOutputStream;
30 import java.io.FileReader;
31 import java.io.IOException;
32 import java.io.PrintWriter;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.Random;
36 import java.util.Vector;
37
38 import javax.swing.JCheckBoxMenuItem;
39 import javax.swing.JColorChooser;
40 import javax.swing.JInternalFrame;
41 import javax.swing.JMenu;
42 import javax.swing.JMenuItem;
43 import javax.swing.JOptionPane;
44 import javax.swing.event.InternalFrameAdapter;
45 import javax.swing.event.InternalFrameEvent;
46 import javax.swing.event.MenuEvent;
47 import javax.swing.event.MenuListener;
48
49 import jalview.bin.Cache;
50 import jalview.datamodel.Alignment;
51 import jalview.datamodel.AlignmentI;
52 import jalview.datamodel.ColumnSelection;
53 import jalview.datamodel.PDBEntry;
54 import jalview.datamodel.SequenceI;
55 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
56 import jalview.io.AppletFormatAdapter;
57 import jalview.io.JalviewFileChooser;
58 import jalview.io.JalviewFileView;
59 import jalview.schemes.BuriedColourScheme;
60 import jalview.schemes.ColourSchemeI;
61 import jalview.schemes.HelixColourScheme;
62 import jalview.schemes.HydrophobicColourScheme;
63 import jalview.schemes.PurinePyrimidineColourScheme;
64 import jalview.schemes.StrandColourScheme;
65 import jalview.schemes.TaylorColourScheme;
66 import jalview.schemes.TurnColourScheme;
67 import jalview.schemes.ZappoColourScheme;
68 import jalview.structures.models.AAStructureBindingModel;
69 import jalview.util.MessageManager;
70 import jalview.util.Platform;
71 import jalview.ws.dbsources.Pdb;
72
73 /**
74  * GUI elements for handlnig an external chimera display
75  * 
76  * @author jprocter
77  *
78  */
79 public class ChimeraViewFrame extends StructureViewerBase
80 {
81   private JalviewChimeraBinding jmb;
82
83   private boolean allChainsSelected = false;
84
85   private boolean alignAddedStructures = false;
86
87   /*
88    * state flag for PDB retrieval thread
89    */
90   private boolean _started = false;
91
92   private boolean addingStructures = false;
93
94   private IProgressIndicator progressBar = null;
95
96   /*
97    * pdb retrieval thread.
98    */
99   private Thread worker = null;
100
101   /*
102    * Path to Chimera session file. This is set when an open Jalview/Chimera
103    * session is saved, or on restore from a Jalview project (if it holds the
104    * filename of any saved Chimera sessions).
105    */
106   private String chimeraSessionFile = null;
107
108   private Random random = new Random();
109
110   /**
111    * Initialise menu options.
112    */
113   private void initMenus()
114   {
115     viewerActionMenu.setText(MessageManager.getString("label.chimera"));
116     viewerColour.setText(MessageManager
117             .getString("label.colour_with_chimera"));
118     viewerColour.setToolTipText(MessageManager
119             .getString("label.let_chimera_manage_structure_colours"));
120     helpItem.setText(MessageManager.getString("label.chimera_help"));
121     seqColour.setSelected(jmb.isColourBySequence());
122     viewerColour.setSelected(!jmb.isColourBySequence());
123     if (_colourwith == null)
124     {
125       _colourwith = new Vector<AlignmentPanel>();
126     }
127     if (_alignwith == null)
128     {
129       _alignwith = new Vector<AlignmentPanel>();
130     }
131
132     // save As not yet implemented
133     savemenu.setVisible(false);
134
135     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
136             MessageManager.getString("label.colour_by"), this, _colourwith,
137             new ItemListener()
138             {
139               @Override
140               public void itemStateChanged(ItemEvent e)
141               {
142                 if (!seqColour.isSelected())
143                 {
144                   seqColour.doClick();
145                 }
146                 else
147                 {
148                   // update the Chimera display now.
149                   seqColour_actionPerformed(null);
150                 }
151               }
152             });
153     viewMenu.add(seqColourBy);
154     final ItemListener handler;
155     JMenu alpanels = new ViewSelectionMenu(
156             MessageManager.getString("label.superpose_with"), this,
157             _alignwith, handler = new ItemListener()
158             {
159               @Override
160               public void itemStateChanged(ItemEvent e)
161               {
162                 alignStructs.setEnabled(_alignwith.size() > 0);
163                 alignStructs.setToolTipText(MessageManager
164                         .formatMessage(
165                                 "label.align_structures_using_linked_alignment_views",
166                                 new Object[]
167                                 { new Integer(_alignwith.size()).toString() }));
168               }
169             });
170     handler.itemStateChanged(null);
171     viewerActionMenu.add(alpanels);
172     viewerActionMenu.addMenuListener(new MenuListener()
173     {
174
175       @Override
176       public void menuSelected(MenuEvent e)
177       {
178         handler.itemStateChanged(null);
179       }
180
181       @Override
182       public void menuDeselected(MenuEvent e)
183       {
184         // TODO Auto-generated method stub
185       }
186
187       @Override
188       public void menuCanceled(MenuEvent e)
189       {
190         // TODO Auto-generated method stub
191       }
192     });
193   }
194
195   /**
196    * add a single PDB structure to a new or existing Chimera view
197    * 
198    * @param pdbentry
199    * @param seq
200    * @param chains
201    * @param ap
202    */
203   public ChimeraViewFrame(PDBEntry pdbentry, SequenceI[] seq,
204           String[] chains, final AlignmentPanel ap)
205   {
206     super();
207     progressBar = ap.alignFrame;
208     // ////////////////////////////////
209     // Is the pdb file already loaded?
210     String alreadyMapped = ap.getStructureSelectionManager()
211             .alreadyMappedToFile(pdbentry.getId());
212
213     if (alreadyMapped != null)
214     {
215       int option = JOptionPane.showInternalConfirmDialog(Desktop.desktop,
216               MessageManager.formatMessage(
217                       "label.pdb_entry_is_already_displayed", new Object[]
218                       { pdbentry.getId() }), MessageManager.formatMessage(
219                       "label.map_sequences_to_visible_window", new Object[]
220                       { pdbentry.getId() }),
221               JOptionPane.YES_NO_CANCEL_OPTION);
222
223       if (option == JOptionPane.CANCEL_OPTION)
224       {
225         return;
226       }
227       if (option == JOptionPane.YES_OPTION)
228       {
229         // TODO : Fix multiple seq to one chain issue here.
230         ap.getStructureSelectionManager().setMapping(seq, chains,
231                 alreadyMapped, AppletFormatAdapter.FILE);
232         if (ap.getSeqPanel().seqCanvas.fr != null)
233         {
234           ap.getSeqPanel().seqCanvas.fr.featuresAdded();
235           ap.paintAlignment(true);
236         }
237
238         // Now this ChimeraViewFrame is mapped to new sequences. We must add
239         // them to the existing array
240         JInternalFrame[] frames = Desktop.instance.getAllFrames();
241
242         for (JInternalFrame frame : frames)
243         {
244           if (frame instanceof ChimeraViewFrame)
245           {
246             final ChimeraViewFrame topView = ((ChimeraViewFrame) frame);
247             // JBPNOTE: this looks like a binding routine, rather than a gui
248             // routine
249             for (int pe = 0; pe < topView.jmb.getPdbCount(); pe++)
250             {
251               if (topView.jmb.getPdbEntry(pe).getFile()
252                       .equals(
253                       alreadyMapped))
254               {
255                 topView.jmb.addSequence(pe, seq);
256                 topView.addAlignmentPanel(ap);
257                 // add it to the set used for colouring
258                 topView.useAlignmentPanelForColourbyseq(ap);
259                 topView.buildActionMenu();
260                 ap.getStructureSelectionManager()
261                         .sequenceColoursChanged(ap);
262                 break;
263               }
264             }
265           }
266         }
267
268         return;
269       }
270     }
271     // /////////////////////////////////
272     // Check if there are other Chimera views involving this alignment
273     // and prompt user about adding this molecule to one of them
274     List<ChimeraViewFrame> existingViews = getChimeraWindowsFor(ap);
275     for (ChimeraViewFrame topView : existingViews)
276     {
277       // TODO: highlight topView in view somehow
278       /*
279        * JAL-1742 exclude view with this structure already mapped (don't offer
280        * to align chain B to chain A of the same structure)
281        */
282       if (topView.hasPdbId(pdbentry.getId()))
283       {
284         continue;
285       }
286       int option = JOptionPane.showInternalConfirmDialog(Desktop.desktop,
287               MessageManager.formatMessage("label.add_pdbentry_to_view",
288                       new Object[]
289                       { pdbentry.getId(), topView.getTitle() }),
290               MessageManager
291                       .getString("label.align_to_existing_structure_view"),
292               JOptionPane.YES_NO_CANCEL_OPTION);
293       if (option == JOptionPane.CANCEL_OPTION)
294       {
295         return;
296       }
297       if (option == JOptionPane.YES_OPTION)
298       {
299         topView.useAlignmentPanelForSuperposition(ap);
300         topView.addStructure(pdbentry, seq, chains, true, ap.alignFrame);
301         return;
302       }
303     }
304     // /////////////////////////////////
305     openNewChimera(ap, new PDBEntry[]
306     { pdbentry }, new SequenceI[][]
307     { seq });
308   }
309
310   /**
311    * Create a helper to manage progress bar display
312    */
313   protected void createProgressBar()
314   {
315     if (progressBar == null)
316     {
317       progressBar = new ProgressBar(statusPanel, statusBar);
318     }
319   }
320
321   protected boolean hasPdbId(String pdbId)
322   {
323     return jmb.hasPdbId(pdbId);
324   }
325
326   private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
327           SequenceI[][] seqs)
328   {
329     createProgressBar();
330
331     String[][] chains = extractChains(seqs);
332     jmb = new JalviewChimeraBindingModel(this,
333             ap.getStructureSelectionManager(), pdbentrys, seqs, chains,
334             null);
335     addAlignmentPanel(ap);
336     useAlignmentPanelForColourbyseq(ap);
337     if (pdbentrys.length > 1)
338     {
339       alignAddedStructures = true;
340       useAlignmentPanelForSuperposition(ap);
341     }
342     jmb.setColourBySequence(true);
343     setSize(400, 400); // probably should be a configurable/dynamic default here
344     initMenus();
345
346     addingStructures = false;
347     worker = new Thread(this);
348     worker.start();
349
350     this.addInternalFrameListener(new InternalFrameAdapter()
351     {
352       public void internalFrameClosing(InternalFrameEvent internalFrameEvent)
353       {
354         closeViewer(false);
355       }
356     });
357
358   }
359
360   /**
361    * Retrieve chains for sequences by inspecting their PDB refs. The hope is
362    * that the first will be to the sequence's own chain. Really need a more
363    * managed way of doing this.
364    * 
365    * @param seqs
366    * @return
367    */
368   protected String[][] extractChains(SequenceI[][] seqs)
369   {
370     String[][] chains = new String[seqs.length][];
371     for (int i = 0; i < seqs.length; i++)
372     {
373       chains[i] = new String[seqs[i].length];
374       int seqno = 0;
375       for (SequenceI seq : seqs[i])
376       {
377         String chain = null;
378         if (seq.getDatasetSequence() != null)
379         {
380           Vector<PDBEntry> pdbrefs = seq.getDatasetSequence().getPDBId();
381           if (pdbrefs != null && pdbrefs.size() > 0)
382           {
383             chain = pdbrefs.get(0).getChainCode();
384           }
385         }
386         chains[i][seqno++] = chain;
387       }
388     }
389     return chains;
390   }
391
392   /**
393    * create a new viewer containing several structures superimposed using the
394    * given alignPanel.
395    * 
396    * @param ap
397    * @param pe
398    * @param seqs
399    */
400   public ChimeraViewFrame(AlignmentPanel ap, PDBEntry[] pe,
401           SequenceI[][] seqs)
402   {
403     super();
404     openNewChimera(ap, pe, seqs);
405   }
406
407   /**
408    * Create a new viewer from saved session state data including Chimera session
409    * file.
410    * 
411    * @param chimeraSession
412    * 
413    * @param alignPanel
414    * @param pdbArray
415    * @param seqsArray
416    * @param colourByChimera
417    * @param colourBySequence
418    */
419   public ChimeraViewFrame(String chimeraSession, AlignmentPanel alignPanel,
420           PDBEntry[] pdbArray,
421           SequenceI[][] seqsArray, boolean colourByChimera,
422           boolean colourBySequence)
423   {
424     super();
425     this.chimeraSessionFile = chimeraSession;
426     openNewChimera(alignPanel, pdbArray, seqsArray);
427     if (colourByChimera)
428     {
429       jmb.setColourBySequence(false);
430       seqColour.setSelected(false);
431       viewerColour.setSelected(true);
432     }
433     else if (colourBySequence)
434     {
435       jmb.setColourBySequence(true);
436       seqColour.setSelected(true);
437       viewerColour.setSelected(false);
438     }
439   }
440
441   /**
442    * add a new structure (with associated sequences and chains) to this viewer,
443    * retrieving it if necessary first.
444    * 
445    * @param pdbentry
446    * @param seq
447    * @param chains
448    * @param alignFrame
449    * @param align
450    *          if true, new structure(s) will be align using associated alignment
451    */
452   private void addStructure(final PDBEntry pdbentry, final SequenceI[] seq,
453           final String[] chains, final boolean b,
454           final IProgressIndicator alignFrame)
455   {
456     if (pdbentry.getFile() == null)
457     {
458       if (worker != null && worker.isAlive())
459       {
460         // a retrieval is in progress, wait around and add ourselves to the
461         // queue.
462         new Thread(new Runnable()
463         {
464           public void run()
465           {
466             while (worker != null && worker.isAlive() && _started)
467             {
468               try
469               {
470                 Thread.sleep(100 + ((int) Math.random() * 100));
471
472               } catch (Exception e)
473               {
474               }
475
476             }
477             // and call ourselves again.
478             addStructure(pdbentry, seq, chains, b, alignFrame);
479           }
480         }).start();
481         return;
482       }
483     }
484     // otherwise, start adding the structure.
485     jmb.addSequenceAndChain(new PDBEntry[]
486     { pdbentry }, new SequenceI[][]
487     { seq }, new String[][]
488     { chains });
489     addingStructures = true;
490     _started = false;
491     alignAddedStructures = b;
492     progressBar = alignFrame; // visual indication happens on caller frame.
493     (worker = new Thread(this)).start();
494     return;
495   }
496
497   private List<ChimeraViewFrame> getChimeraWindowsFor(AlignmentPanel apanel)
498   {
499     List<ChimeraViewFrame> result = new ArrayList<ChimeraViewFrame>();
500     JInternalFrame[] frames = Desktop.instance.getAllFrames();
501
502     for (JInternalFrame frame : frames)
503     {
504       if (frame instanceof ChimeraViewFrame)
505       {
506         if (((StructureViewerBase) frame).isLinkedWith(apanel))
507         {
508           result.add((ChimeraViewFrame) frame);
509         }
510       }
511     }
512     return result;
513   }
514
515   /**
516    * Launch Chimera. If we have a chimera session file name, send Chimera the
517    * command to open its saved session file.
518    */
519   void initChimera()
520   {
521     jmb.setFinishedInit(false);
522     jalview.gui.Desktop.addInternalFrame(this, jmb.getViewerTitle("Chimera", true),
523             getBounds().width, getBounds().height);
524
525     /*
526      * Pass an empty 'command' to launch Chimera
527      */
528     jmb.evalStateCommand("", false);
529
530     if (this.chimeraSessionFile != null)
531     {
532       boolean opened = jmb.openSession(chimeraSessionFile);
533       if (!opened)
534       {
535         System.err
536                 .println("An error occurred opening Chimera session file "
537                         + chimeraSessionFile);
538       }
539     }
540     jmb.setFinishedInit(true);
541
542     jmb.startChimeraListener();
543   }
544
545   void setChainMenuItems(List<String> chainNames)
546   {
547     chainMenu.removeAll();
548     if (chainNames == null)
549     {
550       return;
551     }
552     JMenuItem menuItem = new JMenuItem(
553             MessageManager.getString("label.all"));
554     menuItem.addActionListener(new ActionListener()
555     {
556       public void actionPerformed(ActionEvent evt)
557       {
558         allChainsSelected = true;
559         for (int i = 0; i < chainMenu.getItemCount(); i++)
560         {
561           if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
562           {
563             ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
564           }
565         }
566         centerViewer();
567         allChainsSelected = false;
568       }
569     });
570
571     chainMenu.add(menuItem);
572
573     for (String chainName : chainNames)
574     {
575       menuItem = new JCheckBoxMenuItem(chainName, true);
576       menuItem.addItemListener(new ItemListener()
577       {
578         public void itemStateChanged(ItemEvent evt)
579         {
580           if (!allChainsSelected)
581           {
582             centerViewer();
583           }
584         }
585       });
586
587       chainMenu.add(menuItem);
588     }
589   }
590
591   void centerViewer()
592   {
593     List<String> toshow = new ArrayList<String>();
594     for (int i = 0; i < chainMenu.getItemCount(); i++)
595     {
596       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
597       {
598         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
599         if (item.isSelected())
600         {
601           toshow.add(item.getText());
602         }
603       }
604     }
605     jmb.centerViewer(toshow);
606   }
607
608   /**
609    * Close down this instance of Jalview's Chimera viewer, giving the user the
610    * option to close the associated Chimera window (process). They may wish to
611    * keep it open until they have had an opportunity to save any work.
612    * 
613    * @param closeChimera
614    *          if true, close any linked Chimera process; if false, prompt first
615    */
616   public void closeViewer(boolean closeChimera)
617   {
618     if (jmb.isChimeraRunning())
619     {
620       if (!closeChimera)
621       {
622         String prompt = MessageManager.formatMessage(
623                 "label.confirm_close_chimera", new Object[]
624                 { jmb.getViewerTitle("Chimera", false) });
625         prompt = JvSwingUtils.wrapTooltip(true, prompt);
626         int confirm = JOptionPane.showConfirmDialog(this, prompt,
627                 MessageManager.getString("label.close_viewer"),
628                 JOptionPane.YES_NO_OPTION);
629         closeChimera = confirm == JOptionPane.YES_OPTION;
630       }
631       jmb.closeViewer(closeChimera);
632     }
633     setAlignmentPanel(null);
634     _aps.clear();
635     _alignwith.clear();
636     _colourwith.clear();
637     // TODO: check for memory leaks where instance isn't finalised because jmb
638     // holds a reference to the window
639     jmb = null;
640   }
641
642   /**
643    * Open any newly added PDB structures in Chimera, having first fetched data
644    * from PDB (if not already saved).
645    */
646   public void run()
647   {
648     _started = true;
649     // todo - record which pdbids were successfully imported.
650     StringBuilder errormsgs = new StringBuilder(128);
651     StringBuilder files = new StringBuilder(128);
652     List<PDBEntry> filePDB = new ArrayList<PDBEntry>();
653     List<Integer> filePDBpos = new ArrayList<Integer>();
654     PDBEntry thePdbEntry = null;
655     try
656     {
657       String[] curfiles = jmb.getPdbFile(); // files currently in viewer
658       // TODO: replace with reference fetching/transfer code (validate PDBentry
659       // as a DBRef?)
660       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
661       {
662         String file = null;
663         thePdbEntry = jmb.getPdbEntry(pi);
664         if (thePdbEntry.getFile() == null)
665         {
666           /*
667            * Retrieve PDB data, save to file, attach to PDBEntry
668            */
669           file = fetchPdbFile(thePdbEntry);
670           if (file == null)
671           {
672             errormsgs.append("'" + thePdbEntry.getId() + "' ");
673           }
674         }
675         else
676         {
677           /*
678            * Got file already - ignore if already loaded in Chimera.
679            */
680           file = new File(thePdbEntry.getFile()).getAbsoluteFile()
681                   .getPath();
682           if (curfiles != null && curfiles.length > 0)
683           {
684             addingStructures = true; // already files loaded.
685             for (int c = 0; c < curfiles.length; c++)
686             {
687               if (curfiles[c].equals(file))
688               {
689                 file = null;
690                 break;
691               }
692             }
693           }
694         }
695         if (file != null)
696         {
697           filePDB.add(thePdbEntry);
698           filePDBpos.add(Integer.valueOf(pi));
699           files.append(" \"" + Platform.escapeString(file) + "\"");
700         }
701       }
702     } catch (OutOfMemoryError oomerror)
703     {
704       new OOMWarning("Retrieving PDB files: " + thePdbEntry.getId(),
705               oomerror);
706     } catch (Exception ex)
707     {
708       ex.printStackTrace();
709       errormsgs.append("When retrieving pdbfiles for '"
710               + thePdbEntry.getId() + "'");
711     }
712     if (errormsgs.length() > 0)
713     {
714
715       JOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
716               .formatMessage("label.pdb_entries_couldnt_be_retrieved",
717                       new Object[]
718                       { errormsgs.toString() }), MessageManager
719               .getString("label.couldnt_load_file"),
720               JOptionPane.ERROR_MESSAGE);
721     }
722
723     if (files.length() > 0)
724     {
725       if (!addingStructures)
726       {
727         try
728         {
729           initChimera();
730         } catch (Exception ex)
731         {
732           Cache.log.error("Couldn't open Chimera viewer!", ex);
733         }
734       }
735       int num = -1;
736       for (PDBEntry pe : filePDB)
737       {
738         num++;
739         if (pe.getFile() != null)
740         {
741           try
742           {
743             int pos = filePDBpos.get(num).intValue();
744             long startTime = startProgressBar("Chimera "
745                     + MessageManager.getString("status.opening_file"));
746             jmb.openFile(pe);
747             jmb.addSequence(pos, jmb.getSequence()[pos]);
748             File fl = new File(pe.getFile());
749             String protocol = AppletFormatAdapter.URL;
750             try
751             {
752               if (fl.exists())
753               {
754                 protocol = AppletFormatAdapter.FILE;
755               }
756             } catch (Throwable e)
757             {
758             } finally
759             {
760               stopProgressBar("", startTime);
761             }
762             // Explicitly map to the filename used by Chimera ;
763             // TODO: use pe.getId() instead of pe.getFile() ?
764             jmb.getSsm().setMapping(jmb.getSequence()[pos], null,
765                     pe.getFile(),
766                     protocol);
767           } catch (OutOfMemoryError oomerror)
768           {
769             new OOMWarning(
770                     "When trying to open and map structures from Chimera!",
771                     oomerror);
772           } catch (Exception ex)
773           {
774             Cache.log.error("Couldn't open " + pe.getFile()
775                     + " in Chimera viewer!", ex);
776           } finally
777           {
778             Cache.log.debug("File locations are " + files);
779           }
780         }
781       }
782       jmb.setFinishedInit(true);
783       jmb.setLoadingFromArchive(false);
784
785       // refresh the sequence colours for the new structure(s)
786       for (AlignmentPanel ap : _colourwith)
787       {
788         jmb.updateColours(ap);
789       }
790       // do superposition if asked to
791       if (alignAddedStructures)
792       {
793         new Thread(new Runnable()
794         {
795           public void run()
796           {
797             alignStructs_withAllAlignPanels();
798           }
799         }).start();
800         alignAddedStructures = false;
801       }
802       addingStructures = false;
803     }
804     _started = false;
805     worker = null;
806   }
807
808   /**
809    * Fetch PDB data and save to a local file. Returns the full path to the file,
810    * or null if fetch fails.
811    * 
812    * @param processingEntry
813    * @return
814    * @throws Exception
815    */
816   private String fetchPdbFile(PDBEntry processingEntry) throws Exception
817   {
818     String filePath = null;
819     Pdb pdbclient = new Pdb();
820     AlignmentI pdbseq = null;
821     String pdbid = processingEntry.getId();
822     long hdl = startProgressBar(MessageManager.formatMessage(
823             "status.fetching_pdb", new Object[]
824             { pdbid }));
825     try
826     {
827       pdbseq = pdbclient.getSequenceRecords(pdbid);
828     } catch (OutOfMemoryError oomerror)
829     {
830       new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
831     } finally
832     {
833       String msg = pdbid + " "
834               + MessageManager.getString("label.state_completed");
835       stopProgressBar(msg, hdl);
836     }
837     /*
838      * If PDB data were saved and are not invalid (empty alignment), return the
839      * file path.
840      */
841     if (pdbseq != null && pdbseq.getHeight() > 0)
842     {
843       // just use the file name from the first sequence's first PDBEntry
844       filePath = new File(pdbseq.getSequenceAt(0).getPDBId()
845               .elementAt(0).getFile()).getAbsolutePath();
846       processingEntry.setFile(filePath);
847     }
848     return filePath;
849   }
850
851   /**
852    * Convenience method to update the progress bar if there is one. Be sure to
853    * call stopProgressBar with the returned handle to remove the message.
854    * 
855    * @param msg
856    * @param handle
857    */
858   public long startProgressBar(String msg)
859   {
860     // TODO would rather have startProgress/stopProgress as the
861     // IProgressIndicator interface
862     long tm = random.nextLong();
863     if (progressBar != null)
864     {
865       progressBar.setProgressBar(msg, tm);
866     }
867     return tm;
868   }
869
870   public void stopProgressBar(String msg, long handle)
871   {
872     if (progressBar != null)
873     {
874       progressBar.setProgressBar(msg, handle);
875     }
876   }
877
878   @Override
879   public void pdbFile_actionPerformed(ActionEvent actionEvent)
880   {
881     JalviewFileChooser chooser = new JalviewFileChooser(
882             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
883
884     chooser.setFileView(new JalviewFileView());
885     chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
886     chooser.setToolTipText(MessageManager.getString("action.save"));
887
888     int value = chooser.showSaveDialog(this);
889
890     if (value == JalviewFileChooser.APPROVE_OPTION)
891     {
892       BufferedReader in = null;
893       try
894       {
895         // TODO: cope with multiple PDB files in view
896         in = new BufferedReader(new FileReader(jmb.getPdbFile()[0]));
897         File outFile = chooser.getSelectedFile();
898
899         PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
900         String data;
901         while ((data = in.readLine()) != null)
902         {
903           if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
904           {
905             out.println(data);
906           }
907         }
908         out.close();
909       } catch (Exception ex)
910       {
911         ex.printStackTrace();
912       } finally
913       {
914         if (in != null)
915         {
916           try
917           {
918             in.close();
919           } catch (IOException e)
920           {
921             e.printStackTrace();
922           }
923         }
924       }
925     }
926   }
927
928   @Override
929   public void viewMapping_actionPerformed(ActionEvent actionEvent)
930   {
931     jalview.gui.CutAndPasteTransfer cap = new jalview.gui.CutAndPasteTransfer();
932     try
933     {
934       for (int pdbe = 0; pdbe < jmb.getPdbCount(); pdbe++)
935       {
936         cap.appendText(jmb.printMapping(jmb.getPdbEntry(pdbe).getFile()));
937         cap.appendText("\n");
938       }
939     } catch (OutOfMemoryError e)
940     {
941       new OOMWarning(
942               "composing sequence-structure alignments for display in text box.",
943               e);
944       cap.dispose();
945       return;
946     }
947     jalview.gui.Desktop.addInternalFrame(cap,
948             MessageManager.getString("label.pdb_sequence_mapping"), 550,
949             600);
950   }
951
952   @Override
953   public void eps_actionPerformed(ActionEvent e)
954   {
955     throw new Error(
956             MessageManager
957                     .getString("error.eps_generation_not_implemented"));
958   }
959
960   @Override
961   public void png_actionPerformed(ActionEvent e)
962   {
963     throw new Error(
964             MessageManager
965                     .getString("error.png_generation_not_implemented"));
966   }
967
968   @Override
969   public void viewerColour_actionPerformed(ActionEvent actionEvent)
970   {
971     if (viewerColour.isSelected())
972     {
973       // disable automatic sequence colouring.
974       jmb.setColourBySequence(false);
975     }
976   }
977
978   @Override
979   public void seqColour_actionPerformed(ActionEvent actionEvent)
980   {
981     jmb.setColourBySequence(seqColour.isSelected());
982     if (_colourwith == null)
983     {
984       _colourwith = new Vector<AlignmentPanel>();
985     }
986     if (jmb.isColourBySequence())
987     {
988       if (!jmb.isLoadingFromArchive())
989       {
990         if (_colourwith.size() == 0 && getAlignmentPanel() != null)
991         {
992           // Make the currently displayed alignment panel the associated view
993           _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
994         }
995       }
996       // Set the colour using the current view for the associated alignframe
997       for (AlignmentPanel ap : _colourwith)
998       {
999         jmb.colourBySequence(ap.av.isShowSequenceFeatures(), ap);
1000       }
1001     }
1002   }
1003
1004   @Override
1005   public void chainColour_actionPerformed(ActionEvent actionEvent)
1006   {
1007     chainColour.setSelected(true);
1008     jmb.colourByChain();
1009   }
1010
1011   @Override
1012   public void chargeColour_actionPerformed(ActionEvent actionEvent)
1013   {
1014     chargeColour.setSelected(true);
1015     jmb.colourByCharge();
1016   }
1017
1018   @Override
1019   public void zappoColour_actionPerformed(ActionEvent actionEvent)
1020   {
1021     zappoColour.setSelected(true);
1022     jmb.setJalviewColourScheme(new ZappoColourScheme());
1023   }
1024
1025   @Override
1026   public void taylorColour_actionPerformed(ActionEvent actionEvent)
1027   {
1028     taylorColour.setSelected(true);
1029     jmb.setJalviewColourScheme(new TaylorColourScheme());
1030   }
1031
1032   @Override
1033   public void hydroColour_actionPerformed(ActionEvent actionEvent)
1034   {
1035     hydroColour.setSelected(true);
1036     jmb.setJalviewColourScheme(new HydrophobicColourScheme());
1037   }
1038
1039   @Override
1040   public void helixColour_actionPerformed(ActionEvent actionEvent)
1041   {
1042     helixColour.setSelected(true);
1043     jmb.setJalviewColourScheme(new HelixColourScheme());
1044   }
1045
1046   @Override
1047   public void strandColour_actionPerformed(ActionEvent actionEvent)
1048   {
1049     strandColour.setSelected(true);
1050     jmb.setJalviewColourScheme(new StrandColourScheme());
1051   }
1052
1053   @Override
1054   public void turnColour_actionPerformed(ActionEvent actionEvent)
1055   {
1056     turnColour.setSelected(true);
1057     jmb.setJalviewColourScheme(new TurnColourScheme());
1058   }
1059
1060   @Override
1061   public void buriedColour_actionPerformed(ActionEvent actionEvent)
1062   {
1063     buriedColour.setSelected(true);
1064     jmb.setJalviewColourScheme(new BuriedColourScheme());
1065   }
1066
1067   @Override
1068   public void purinePyrimidineColour_actionPerformed(ActionEvent actionEvent)
1069   {
1070     setJalviewColourScheme(new PurinePyrimidineColourScheme());
1071   }
1072
1073   @Override
1074   public void userColour_actionPerformed(ActionEvent actionEvent)
1075   {
1076     userColour.setSelected(true);
1077     new UserDefinedColours(this, null);
1078   }
1079
1080   @Override
1081   public void backGround_actionPerformed(ActionEvent actionEvent)
1082   {
1083     java.awt.Color col = JColorChooser
1084             .showDialog(this, MessageManager
1085                     .getString("label.select_backgroud_colour"), null);
1086     if (col != null)
1087     {
1088       jmb.setBackgroundColour(col);
1089     }
1090   }
1091
1092   @Override
1093   public void showHelp_actionPerformed(ActionEvent actionEvent)
1094   {
1095     try
1096     {
1097       jalview.util.BrowserLauncher
1098               .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
1099     } catch (Exception ex)
1100     {
1101     }
1102   }
1103
1104   public void updateTitleAndMenus()
1105   {
1106     if (jmb.fileLoadingError != null && jmb.fileLoadingError.length() > 0)
1107     {
1108       repaint();
1109       return;
1110     }
1111     setChainMenuItems(jmb.getChainNames());
1112
1113     this.setTitle(jmb.getViewerTitle("Chimera", true));
1114     if (jmb.getPdbFile().length > 1 && jmb.getSequence().length > 1)
1115     {
1116       viewerActionMenu.setVisible(true);
1117     }
1118     if (!jmb.isLoadingFromArchive())
1119     {
1120       seqColour_actionPerformed(null);
1121     }
1122   }
1123
1124   /*
1125    * (non-Javadoc)
1126    * 
1127    * @see
1128    * jalview.jbgui.GStructureViewer#alignStructs_actionPerformed(java.awt.event
1129    * .ActionEvent)
1130    */
1131   @Override
1132   protected void alignStructs_actionPerformed(ActionEvent actionEvent)
1133   {
1134     alignStructs_withAllAlignPanels();
1135   }
1136
1137   private void alignStructs_withAllAlignPanels()
1138   {
1139     if (getAlignmentPanel() == null)
1140     {
1141       return;
1142     }
1143     ;
1144     if (_alignwith.size() == 0)
1145     {
1146       _alignwith.add(getAlignmentPanel());
1147     }
1148     ;
1149     try
1150     {
1151       AlignmentI[] als = new Alignment[_alignwith.size()];
1152       ColumnSelection[] alc = new ColumnSelection[_alignwith.size()];
1153       int[] alm = new int[_alignwith.size()];
1154       int a = 0;
1155
1156       for (AlignmentPanel ap : _alignwith)
1157       {
1158         als[a] = ap.av.getAlignment();
1159         alm[a] = -1;
1160         alc[a++] = ap.av.getColumnSelection();
1161       }
1162       jmb.superposeStructures(als, alm, alc);
1163     } catch (Exception e)
1164     {
1165       StringBuffer sp = new StringBuffer();
1166       for (AlignmentPanel ap : _alignwith)
1167       {
1168         sp.append("'" + ap.alignFrame.getTitle() + "' ");
1169       }
1170       Cache.log.info("Couldn't align structures with the " + sp.toString()
1171               + "associated alignment panels.", e);
1172
1173     }
1174
1175   }
1176
1177   public void setJalviewColourScheme(ColourSchemeI ucs)
1178   {
1179     jmb.setJalviewColourScheme(ucs);
1180
1181   }
1182
1183   /**
1184    * 
1185    * @param alignment
1186    * @return first alignment panel displaying given alignment, or the default
1187    *         alignment panel
1188    */
1189   public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment)
1190   {
1191     for (AlignmentPanel ap : getAllAlignmentPanels())
1192     {
1193       if (ap.av.getAlignment() == alignment)
1194       {
1195         return ap;
1196       }
1197     }
1198     return getAlignmentPanel();
1199   }
1200
1201   @Override
1202   public AAStructureBindingModel getBinding()
1203   {
1204     return jmb;
1205   }
1206
1207   /**
1208    * Ask Chimera to save its session to the designated file path. Returns true
1209    * if successful, else false.
1210    * 
1211    * @param filepath
1212    * @see getStateInfo
1213    */
1214   public boolean saveSession(String filepath)
1215   {
1216     boolean result = jmb.saveSession(filepath);
1217     if (result)
1218     {
1219       this.chimeraSessionFile = filepath;
1220     }
1221     return result;
1222   }
1223
1224   /**
1225    * Returns the file path of the Chimera session file the last time it was
1226    * saved. If it was never saved, returns an empty string. There is no
1227    * guarantee that the Chimera session has not changed since it was saved.
1228    */
1229   @Override
1230   public String getStateInfo()
1231   {
1232     return this.chimeraSessionFile;
1233   }
1234
1235   @Override
1236   protected void fitToWindow_actionPerformed()
1237   {
1238     jmb.focusView();
1239   }
1240 }