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